src/Entity/Zone.php line 35

Open in your IDE?
  1. <?php
  2. namespace App\Entity;
  3. use AdScore\Common\Gearboy\Gearboy;
  4. use AdScore\Common\Definition\Apikey as ApikeyDefinition;
  5. use AdScore\Common\Crypt\{
  6.     Asymmetric\OpenSSL as AsymmOpenSSL,
  7.     Symmetric\AbstractSymmetricCrypt,
  8.     CryptFactory
  9. };
  10. use AdScore\Common\Struct\{
  11.     AbstractStruct,
  12.     StructFactory
  13. };
  14. use AdScore\Common\StrUtils;
  15. use AdScore\Traffic\Utils\Request as TrafficRequest;
  16. use App\Component\Result;
  17. use App\HelperFunctions;
  18. use DateTime;
  19. use DateTimeInterface;
  20. use Doctrine\Common\Collections\ArrayCollection;
  21. use Doctrine\Common\Collections\Collection;
  22. use Doctrine\ORM\Mapping as ORM;
  23. use RuntimeException;
  24. use InvalidArgumentException;
  25. use Symfony\Component\Serializer\Annotation as Serializer;
  26. /**
  27.  * Zone
  28.  *
  29.  * @ORM\Table(name="zones", indexes={@ORM\Index(name="account_id", columns={"account_id"}), @ORM\Index(name="status", columns={"status"})})
  30.  * @ORM\Entity
  31.  */
  32. class Zone extends Entity
  33. {
  34.     public const SERIALIZATION_CONTEXT = [
  35.         'list' => ['groups' => ['general']],
  36.     ];
  37.     public const TRAFFIC_OPTIONS_DEFAULTS = [
  38.         'enable_subid_anomaly_detection' => true,
  39.         'allow_good_bots' => false,
  40.         'allow_google_scanners' => false,
  41.         'enable_anti_piracy_compliance' => false,
  42.         'allow_lite_processing' => false,
  43.         'iframe_handling' => 'auto'/* auto, deny */
  44.         'subid_source' => 'parameter'/* parameter, location_domain, referrer_domain, location_param_utm_source, location_param_utm_campaign, location_param_utm_medium */
  45.         'enable_compliance_intelligence' => true,
  46.         'enable_referrer_blacklisting' => true,
  47.         'mobile_request_as_desktop_traffic' => true
  48.     ];
  49.     const DEFAULT_RESPONSE_AUTH 'hash_sha256';
  50.     
  51.     /**
  52.      * @var int
  53.      *
  54.      * @ORM\Column(name="id", type="bigint", nullable=false, options={"unsigned"=true})
  55.      * @ORM\Id
  56.      * @ORM\GeneratedValue(strategy="IDENTITY")
  57.      * @Serializer\Groups({"general", "account_admin_details"})
  58.      */
  59.     protected $id;
  60.     /**
  61.      * @var DateTime
  62.      *
  63.      * @ORM\Column(name="created", type="datetime", nullable=false)
  64.      * @Serializer\Groups({"general"})
  65.      */
  66.     protected $created null;
  67.     /**
  68.      * @var string
  69.      *
  70.      * @ORM\Column(name="status", type="string", length=0, nullable=false, options={"default"="active"})
  71.      * @Serializer\Groups({"general"})
  72.      */
  73.     protected $status self::STATUS['active'];
  74.     /**
  75.      * @var string
  76.      *
  77.      * @ORM\Column(name="name", type="string", length=255, nullable=false)
  78.      * @Serializer\Groups({"general", "account_admin_details"})
  79.      */
  80.     protected $name;
  81.     /**
  82.      * @var string
  83.      *
  84.      * @ORM\Column(name="apikey_key", type="string", length=64, nullable=false)
  85.      */
  86.     protected $apikeyKey;
  87.     /**
  88.      * @var string|null
  89.      *
  90.      * @ORM\Column(name="request_auth", type="string", length=0, nullable=true)
  91.      * @Serializer\Groups({"general"})
  92.      */
  93.     protected $requestAuth null;
  94.     /**
  95.      * @var string|null
  96.      *
  97.      * @ORM\Column(name="request_pass", type="string", length=64, nullable=true)
  98.      */
  99.     protected $requestPass;
  100.     /**
  101.      * @var string|null
  102.      *
  103.      * @ORM\Column(name="request_key", type="string", length=255, nullable=true)
  104.      */
  105.     protected $requestKey;
  106.     /**
  107.      * @var array|null
  108.      *
  109.      * @ORM\Column(name="request_referrer", type="line_separated_values", length=65535, nullable=true)
  110.      * @Serializer\Groups({"general"})
  111.      */
  112.     protected $requestReferrer null;
  113.     /**
  114.      * @var string
  115.      *
  116.      * @ORM\Column(name="response_auth", type="string", length=0, nullable=true)
  117.      * @Serializer\Groups({"general"})
  118.      */
  119.     protected $responseAuth self::DEFAULT_RESPONSE_AUTH;
  120.     /**
  121.      * @var string|null
  122.      *
  123.      * @ORM\Column(name="response_key", type="string", length=255, nullable=true)
  124.      */
  125.     protected $responseKey;
  126.     /**
  127.      * @var string|null
  128.      *
  129.      * @ORM\Column(name="external_sub_id_url", type="string", length=255, nullable=true)
  130.      * @Serializer\Groups({"general"})
  131.      */
  132.     protected $externalSubIdUrl null;
  133.     /**
  134.      * @var string|null
  135.      *
  136.      * @ORM\Column(name="result_source", type="string", length=64, nullable=true)
  137.      * @Serializer\Groups({"general"})
  138.      */
  139.     protected $resultSource;
  140.     /**
  141.      * @var object|null
  142.      *
  143.      * @ORM\Column(name="traffic_options", type="json", nullable=true)
  144.      */
  145.     protected $trafficOptions self::TRAFFIC_OPTIONS_DEFAULTS;
  146.     /**
  147.      * @var Account|null
  148.      *
  149.      * @ORM\ManyToOne(targetEntity="Account")
  150.      * @ORM\JoinColumns({
  151.      *   @ORM\JoinColumn(name="account_id", referencedColumnName="id")
  152.      * })
  153.      */
  154.     protected $account;
  155.     /**
  156.      * @var ArrayCollection
  157.      *
  158.      * @ORM\OneToMany(targetEntity="GwocDomain", mappedBy="account")
  159.      */
  160.     protected $gwocDomains;
  161.     /**
  162.      * @var bool
  163.      */
  164.     protected $hasSubId true;
  165.     /**
  166.      * @var bool
  167.      */
  168.     protected $hasTraffic true;
  169.     /**
  170.      * @var array
  171.      * @Serializer\Ignore()
  172.      */
  173.     private $liveData;
  174.     /**
  175.      * @var array|false
  176.      */
  177.     private $ltam;
  178.     /**
  179.      * @var string|null
  180.      * @ORM\Column(type="string", length=255)
  181.      * @Serializer\Groups({"general"})
  182.      */
  183.     private $customFields;
  184.     public function __construct(int $userIdAccount $account)
  185.     {
  186.         $this->gwocDomains = new ArrayCollection();
  187.         $this->frontInvoker()->privilegeCheck($userId$account->getId(), 'owner');
  188.         $this->setAccount($account);
  189.         $this->apikeyKey random_bytes(32);
  190.         $this->requestKey random_bytes(32);
  191.         $this->requestPass random_bytes(32);
  192.         $this->generateResponseKey();
  193.         $this->created = new DateTime();
  194.     }
  195.     /**
  196.      * @param int $id
  197.      */
  198.     public function setId(int $id): void
  199.     {
  200.         $this->id $id;
  201.     }
  202.     public function getId(): ?int
  203.     {
  204.         return $this->id;
  205.     }
  206.     public function getCreated(): ?DateTimeInterface
  207.     {
  208.         return $this->created;
  209.     }
  210.     public function setCreated(DateTimeInterface $created): self
  211.     {
  212.         $this->created $created;
  213.         return $this;
  214.     }
  215.     public function getStatus(): ?string
  216.     {
  217.         return $this->status;
  218.     }
  219.     public function setStatus(string $status): self
  220.     {
  221.         $this->status $status;
  222.         return $this;
  223.     }
  224.     public function getName(): ?string
  225.     {
  226.         return $this->name;
  227.     }
  228.     public function setName(string $name): self
  229.     {
  230.         $this->name trim($name);
  231.         return $this;
  232.     }
  233.     /**
  234.      * This is the "apikey key" (sic!) - not the apikey used by the app, but an intermediate seed used to build and verify API key
  235.      * @return string|null
  236.      * @Serializer\Ignore
  237.      */
  238.     public function getApikeyKey(): ?string
  239.     {
  240.         return $this->apikeyKey;
  241.     }
  242.     /**
  243.      * @return string|null
  244.      * @Serializer\Ignore
  245.      */
  246.     public function getEncodedApikeyKey(): ?string
  247.     {
  248.         return StrUtils::toBase64($this->apikeyKey);
  249.     }
  250.     public function generateApikey(): string {
  251.         $ak = new ApikeyDefinition($this->id$this->apikeyKey);
  252.         return $ak->toString();
  253.     }
  254.     /**
  255.      * This is the "apikey key" (sic!) - not the apikey used by the app, but an intermediate seed used to build and verify API key
  256.      * @param string $apikeyKey
  257.      * @return self
  258.      */
  259.     public function setApikeyKey(string $apikeyKey): self
  260.     {
  261.         $this->apikeyKey $apikeyKey;
  262.         return $this;
  263.     }
  264.     public function getRequestAuth(): ?string
  265.     {
  266.         return $this->requestAuth;
  267.     }
  268.     public function setRequestAuth(?string $requestAuth): self
  269.     {
  270.         $this->requestAuth $requestAuth;
  271.         return $this;
  272.     }
  273.     /**
  274.      * @return string|null
  275.      * @Serializer\Ignore
  276.      */
  277.     public function getRequestPass(): ?string
  278.     {
  279.         return $this->requestPass;
  280.     }
  281.     /**
  282.      * @return string|null
  283.      * @Serializer\Groups({"encoded", "sign", "secure"})
  284.      * @Serializer\SerializedName("request_pass")
  285.      */
  286.     public function getEncodedRequestPass(): ?string
  287.     {
  288.         return StrUtils::toBase64($this->requestPass);
  289.     }
  290.     public function setRequestPass(?string $requestPass): self
  291.     {
  292.         $this->requestPass $requestPass;
  293.         return $this;
  294.     }
  295.     /**
  296.      * @return string|null
  297.      * @Serializer\Ignore
  298.      */
  299.     public function getRequestKey(): ?string
  300.     {
  301.         return $this->requestKey;
  302.     }
  303.     /**
  304.      * @return string|null
  305.      * @Serializer\Groups({"encoded", "sign", "secure"})
  306.      * @Serializer\SerializedName("request_key")
  307.      */
  308.     public function getEncodedRequestKey(): ?string
  309.     {
  310.         return StrUtils::toBase64($this->requestKey);
  311.     }
  312.     public function setRequestKey(?string $requestKey): self
  313.     {
  314.         $this->requestKey $requestKey;
  315.         return $this;
  316.     }
  317.     public function getRequestReferrer(): ?array
  318.     {
  319.         return $this->requestReferrer;
  320.     }
  321.     /**
  322.      * @param null|array|string $requestReferrer
  323.      * @return $this
  324.      */
  325.     public function setRequestReferrer($requestReferrer): self
  326.     {
  327.         if (is_string($requestReferrer)) {
  328.             $requestReferrer = [$requestReferrer];
  329.         }
  330.         $this->requestReferrer $requestReferrer;
  331.         return $this;
  332.     }
  333.     protected function validateResponseAuth(string $responseAuth) : bool {
  334.         /* Legacy v4 response auth types */
  335.         if (in_array($responseAuth, ['hash_sha256''sign_sha256'])) {
  336.             return true;
  337.         }
  338.         $matches = [];
  339.         /* [1] version, [2] crypt, [3] struct */
  340.         if (!preg_match('/^v(\d{1})\_([a-f\d]{4})([A-Z])$/'$responseAuth$matches)) {
  341.             throw new InvalidArgumentException('Invalid response auth specifier format "' $responseAuth '"');
  342.         }
  343.         [, $version$crypt$struct] = $matches;
  344.         if ($version !== '5') {
  345.             throw new InvalidArgumentException('Unsupported response auth version "' $responseAuth '"');
  346.         }
  347.         $cryptId = \hexdec($crypt);
  348.         $cryptInst CryptFactory::createFromId($cryptId);
  349.         $structInst StructFactory::create($struct);
  350.         return ($cryptInst instanceof AbstractSymmetricCrypt) && ($structInst instanceof AbstractStruct);
  351.     }
  352.     
  353.     public function getResponseAuth(): string
  354.     {
  355.         return $this->responseAuth ?? self::DEFAULT_RESPONSE_AUTH;
  356.     }
  357.     public function setResponseAuth(string $responseAuth): self
  358.     {
  359.         if ($responseAuth !== $this->responseAuth) {
  360.             $this->validateResponseAuth($responseAuth);
  361.             $this->responseAuth $responseAuth;
  362.             $this->generateResponseKey();
  363.         }
  364.         return $this;
  365.     }
  366.     /**
  367.      * @return string|null
  368.      * @Serializer\Ignore
  369.      */
  370.     public function getResponseKey(): ?string
  371.     {
  372.         return $this->responseKey;
  373.     }
  374.     /**
  375.      * @return string|null
  376.      * @Serializer\Groups({"secure"})
  377.      * @Serializer\SerializedName("response_key")
  378.      */
  379.     public function getEncodedResponseKey(): ?string
  380.     {
  381.         return StrUtils::toBase64($this->responseKey);
  382.     }
  383.     /**
  384.      * @return string|null
  385.      * @Serializer\Groups({"sign"})
  386.      * @Serializer\SerializedName("response_key")
  387.      */
  388.     public function getPublicResponseKey(): ?string
  389.     {
  390.         if ($this->hasAsymmetricResponseKey()) {
  391.             return AsymmOpenSSL::getPublicKeyPem($this->responseKey);
  392.         }
  393.         return null;
  394.     }
  395.     public function setResponseKey(?string $responseKey): self
  396.     {
  397.         $this->responseKey $responseKey;
  398.         return $this;
  399.     }
  400.     public function getExternalSubIdUrl(): ?string
  401.     {
  402.         return $this->externalSubIdUrl;
  403.     }
  404.     public function setExternalSubIdUrl(?string $externalSubIdUrl): self
  405.     {
  406.         if (empty(trim($externalSubIdUrl))) {
  407.             $this->externalSubIdUrl null;
  408.         } else {
  409.             $this->externalSubIdUrl $externalSubIdUrl;
  410.         }
  411.         return $this;
  412.     }
  413.     public function getResultSource(): ?string
  414.     {
  415.         return $this->resultSource;
  416.     }
  417.     public function setResultSource(?string $resultSource): self
  418.     {
  419.         $this->resultSource $resultSource;
  420.         return $this;
  421.     }
  422.     /**
  423.      * @return array
  424.      * @throws \Exception
  425.      * @Serializer\Groups({"general"})
  426.      */
  427.     public function getTrafficOptions(): array
  428.     {
  429.         return (array) HelperFunctions::setStdDefaults((object) $this->trafficOptionsself::TRAFFIC_OPTIONS_DEFAULTS);
  430.     }
  431.     public function setTrafficOptions(?object $trafficOptions): self
  432.     {
  433.         $this->trafficOptions HelperFunctions::setStdDefaults((object) $trafficOptions$this->getTrafficOptions());
  434.         return $this;
  435.     }
  436.     /**
  437.      * @return Account|null
  438.      * @Serializer\Ignore()
  439.      */
  440.     public function getAccount(): ?Account
  441.     {
  442.         return $this->account;
  443.     }
  444.     public function setAccount(?Account $account): self
  445.     {
  446.         $this->account $account;
  447.         return $this;
  448.     }
  449.     /**
  450.      * @return int|null
  451.      * @Serializer\Groups({"general"})
  452.      */
  453.     public function getAccountId(): ?int
  454.     {
  455.         return ($this->account === null) ? null $this->account->getId();
  456.     }
  457.     /**
  458.      * @return Collection<int, GwocDomain>
  459.      * @Serializer\Ignore()
  460.      */
  461.     public function getGwocDomains(): Collection
  462.     {
  463.         return $this->gwocDomains;
  464.     }
  465.     public function addGwocDomain(GwocDomain $gwocDomain): self
  466.     {
  467.         if (!$this->gwocDomains->contains($gwocDomain)) {
  468.             $this->gwocDomains[] = $gwocDomain;
  469.             $gwocDomain->setZone($this);
  470.             $gwocDomain->setAccount($this->getAccount());
  471.         }
  472.         return $this;
  473.     }
  474.     public function removeGwocDomain(GwocDomain $gwocDomain): self
  475.     {
  476.         if ($this->gwocDomains->removeElement($gwocDomain)) {
  477.             // set the owning side to null (unless already changed)
  478.             if ($gwocDomain->getAccount() === $this->getAccount()) {
  479.                 $gwocDomain->setAccount(null);
  480.             }
  481.             if ($gwocDomain->getZone() === $this) {
  482.                 $gwocDomain->setZone(null);
  483.             }
  484.         }
  485.         return $this;
  486.     }
  487.     public function hasAsymmetricResponseKey(): bool
  488.     {
  489.         return StrUtils::startsWith($this->responseAuth ?? '''sign_');
  490.     }
  491.     /**
  492.      * @return string|null
  493.      * @Serializer\Groups({"general"})
  494.      */
  495.     public function getEffExternalSubIdUrl(): ?string
  496.     {
  497.         return $this->externalSubIdUrl ?? $this->account->getExternalSubIdUrl();
  498.     }
  499.     public function fetchLiveData(int $userId): void
  500.     {
  501.         if (empty($this->id)) {
  502.             throw new RuntimeException('Cannot fetch live data for non-existent zone');
  503.         }
  504.         $this->liveData TrafficRequest::encodeLive(Result::current($this->frontInvoker()->zoneGetById($userId$this->account->getId(), $this->getId()), 'zone'));
  505.     }
  506.     public function checkPrivileges(int $userIdint $accountId$action null): void
  507.     {
  508.         $this->frontInvoker()->zonePrivilegeCheck($userId$accountId$this->getId());
  509.     }
  510.     public function generateResponseKey(): void
  511.     {
  512.         if ($this->hasAsymmetricResponseKey()) {
  513.             $this->responseKey AsymmOpenSSL::createEcPrivateKey();
  514.         } else {
  515.             $this->responseKey random_bytes(32);
  516.         }
  517.     }
  518.     public function fetchLtam(): void
  519.     {
  520.         if (empty($this->id)) {
  521.             throw new RuntimeException('Cannot fetch traffic data for non-existent zone');
  522.         }
  523.         $data $this->frontInvoker()->query('SELECT LOWER(HEX(id)) AS hash, value AS seen FROM adscore.kv_records WHERE zone_id = :zone_id', ['zone_id' => $this->id]);
  524.         $this->ltam array_combine(array_column($data'hash'), array_map('intval'array_column($data'seen')));
  525.     }
  526.     /**
  527.      * Gather traffic data from Clickhouse nodes
  528.      * @return void
  529.      * @throws RuntimeException
  530.      */
  531.     public function fetchTrafficData(): void
  532.     {
  533.         if (empty($this->id)) {
  534.             throw new RuntimeException('Cannot fetch traffic data for non-existent zone');
  535.         }
  536.         $rawData Gearboy::query([
  537.             'has_sub_id' => [
  538.                 'query' => 'SELECT count(DISTINCT sub_id) AS count FROM (SELECT * FROM traffic WHERE (zone_id = :zone_id) AND isNotNull(sub_id) AND (sub_id != \'\') LIMIT 2)',
  539.                 'args' => ['zone_id' => $this->id],
  540.                 'pool' => 'traffic_local_copy',
  541.                 'type' => 'value'
  542.             ],
  543.             'has_traffic' => [
  544.                 'query' => 'SELECT count(*) AS count FROM (SELECT * FROM traffic WHERE (zone_id = :zone_id) LIMIT 2)',
  545.                 'args' => ['zone_id' => $this->id],
  546.                 'pool' => 'traffic_local_copy',
  547.                 'type' => 'value'
  548.             ]
  549.         ]);
  550.         $this->hasSubId = (bool)$rawData['has_sub_id'] ?? false;
  551.         $this->hasTraffic = (bool)$rawData['has_traffic'] ?? false;
  552.     }
  553.     /**
  554.      * @return array|null
  555.      * @Serializer\Ignore()
  556.      */
  557.     public function getLtam(): array
  558.     {
  559.         if ($this->ltam === null) {
  560.             $this->fetchLtam();
  561.         }
  562.         return $this->ltam;
  563.     }
  564.     /**
  565.      * @return bool|null
  566.      * @Serializer\SerializedName("has_sub_id")
  567.      * @Serializer\Groups({"read", "general"})
  568.      */
  569.     public function hasSubId(): ?bool
  570.     {
  571.         return $this->hasSubId;
  572.     }
  573.     /**
  574.      * @return bool|null
  575.      * @Serializer\SerializedName("has_traffic")
  576.      * @Serializer\Groups({"read", "general"})
  577.      */
  578.     public function hasTraffic(): ?bool
  579.     {
  580.         return $this->hasTraffic;
  581.     }
  582.     /**
  583.      * @return array|null
  584.      * @Serializer\Groups({"read", "secure"})
  585.      */
  586.     public function getApikey(): ?string
  587.     {
  588.         return $this->liveData['apikey'] ?? $this->generateApikey();
  589.     }
  590.     /**
  591.      * @return array|null
  592.      * @Serializer\Groups({"read"})
  593.      */
  594.     public function isApikeyActive(): ?bool
  595.     {
  596.         return $this->liveData['apikey_active'] ?? null;
  597.     }
  598.     /**
  599.      * @return array|null
  600.      */
  601.     public function getCustomFields(): array
  602.     {
  603.         if (empty($this->customFields)) {
  604.             return [];
  605.         }
  606.         return explode(','$this->customFields);
  607.     }
  608.     /**
  609.      * @param array|null $customFields
  610.      * @return Zone
  611.      */
  612.     public function setCustomFields(?array $customFields): Zone
  613.     {
  614.         $customFields implode(','array_unique((array) $customFields));
  615.         $this->customFields = empty($customFields) ? null $customFields;
  616.         return $this;
  617.     }
  618. }