Click.php
8 months ago
Click_Processing_Job.php
5 months ago
Config_File_Manager.php
1 year ago
Link_Rule.php
1 year ago
Link_Rule_Finder.php
9 months ago
Click_Processing_Job.php
129 lines
| 1 | <?php |
| 2 | |
| 3 | namespace IAWP\Click_Tracking; |
| 4 | |
| 5 | use IAWPSCOPED\Carbon\CarbonImmutable; |
| 6 | use IAWP\Cron_Job; |
| 7 | use IAWP\Models\Visitor; |
| 8 | use IAWP\Payload_Validator; |
| 9 | use IAWP\Utils\Security; |
| 10 | use IAWPSCOPED\Illuminate\Support\Str; |
| 11 | use IAWPSCOPED\League\Uri\Uri; |
| 12 | /** @internal */ |
| 13 | class Click_Processing_Job extends Cron_Job |
| 14 | { |
| 15 | protected $name = 'iawp_click_processing'; |
| 16 | protected $interval = 'every_minute'; |
| 17 | public function handle() : void |
| 18 | { |
| 19 | // Periodically recreate the config file |
| 20 | \IAWP\Click_Tracking\Config_File_Manager::recreate(); |
| 21 | if (\IAWPSCOPED\iawp_is_free()) { |
| 22 | self::unschedule(); |
| 23 | return; |
| 24 | } |
| 25 | $click_data_file = $this->get_click_data_file(); |
| 26 | if ($click_data_file === null) { |
| 27 | return; |
| 28 | } |
| 29 | $job_file = $this->create_job_file($click_data_file); |
| 30 | if ($job_file === null) { |
| 31 | return; |
| 32 | } |
| 33 | $job_handle = \fopen($job_file, 'r'); |
| 34 | if ($job_handle === \false) { |
| 35 | return; |
| 36 | } |
| 37 | // The first line for the PHP file is an exit statement to keep the contents private. This |
| 38 | // should be skipped when parsing the file. |
| 39 | if (\pathinfo($job_file, \PATHINFO_EXTENSION) === 'php') { |
| 40 | \fgets($job_handle); |
| 41 | // Skip first line |
| 42 | } |
| 43 | while (($json = \fgets($job_handle)) !== \false) { |
| 44 | $event = \json_decode($json, \true); |
| 45 | if (\is_null($event)) { |
| 46 | continue; |
| 47 | } |
| 48 | $payload_validator = Payload_Validator::new($event['payload'], $event['signature']); |
| 49 | if (!$payload_validator->is_valid() || \is_null($payload_validator->resource())) { |
| 50 | continue; |
| 51 | } |
| 52 | if (\is_string($event['href']) && !$this->is_valid_href($event['href'])) { |
| 53 | continue; |
| 54 | } |
| 55 | $event['href'] = \sanitize_url($event['href']); |
| 56 | $event['classes'] = Security::string($event['classes']); |
| 57 | $event['ids'] = Security::string($event['ids']); |
| 58 | $click = \IAWP\Click_Tracking\Click::new(['href' => $event['href'], 'classes' => $event['classes'], 'ids' => $event['ids'], 'resource_id' => $payload_validator->resource()['id'], 'visitor_id' => Visitor::fetch_visitor_id_by_hash($event['visitor_token']), 'created_at' => CarbonImmutable::createFromTimestamp($event['created_at'], 'utc')]); |
| 59 | $click->track(); |
| 60 | } |
| 61 | \fclose($job_handle); |
| 62 | \unlink($job_file); |
| 63 | } |
| 64 | private function get_click_data_file() : ?string |
| 65 | { |
| 66 | $avoid_temporary_directory = \defined('IAWP_AVOID_TEMPORARY_DIRECTORY') ? \IAWP_AVOID_TEMPORARY_DIRECTORY === \true : \false; |
| 67 | if (!$avoid_temporary_directory) { |
| 68 | $text_file = Str::finish(\sys_get_temp_dir(), \DIRECTORY_SEPARATOR) . "iawp-click-data.txt"; |
| 69 | if (\is_file($text_file) && \is_readable($text_file) && \is_writable($text_file)) { |
| 70 | return $text_file; |
| 71 | } |
| 72 | } |
| 73 | $php_file = \IAWPSCOPED\iawp_path_to('iawp-click-data.php'); |
| 74 | if (\is_file($php_file)) { |
| 75 | return $php_file; |
| 76 | } |
| 77 | return null; |
| 78 | } |
| 79 | private function create_job_file(string $file) : ?string |
| 80 | { |
| 81 | if (!\is_readable($file) || !\is_writable($file)) { |
| 82 | return null; |
| 83 | } |
| 84 | $job_id = \rand(); |
| 85 | $extension = \pathinfo($file, \PATHINFO_EXTENSION); |
| 86 | $job_file = Str::finish(\dirname($file), \DIRECTORY_SEPARATOR) . "iawp-click-data-{$job_id}.{$extension}"; |
| 87 | $was_renamed = \rename($file, $job_file); |
| 88 | if (!$was_renamed || !\is_file($job_file)) { |
| 89 | return null; |
| 90 | } |
| 91 | return $job_file; |
| 92 | } |
| 93 | private function is_valid_href(string $href) : bool |
| 94 | { |
| 95 | if ($this->has_injection_attempt($href)) { |
| 96 | return \false; |
| 97 | } |
| 98 | $uri = Uri::createFromString($href); |
| 99 | $scheme = $uri->getScheme(); |
| 100 | if ($scheme === null) { |
| 101 | return \false; |
| 102 | } |
| 103 | $is_http = $scheme === 'http' || $scheme === 'https'; |
| 104 | if ($is_http && \filter_var($href, \FILTER_VALIDATE_URL) === \false) { |
| 105 | return \false; |
| 106 | } |
| 107 | return \true; |
| 108 | } |
| 109 | /** |
| 110 | * The goal of this method is not security. That's handled by the secure and robust functions |
| 111 | * that PHP and WordPress provide. The goal of this function is to detect when someone tried |
| 112 | * to use SQL injection so we can ignore the spam clicks. |
| 113 | * |
| 114 | * @param string $string |
| 115 | * |
| 116 | * @return bool |
| 117 | */ |
| 118 | private function has_injection_attempt(string $string) : bool |
| 119 | { |
| 120 | $patterns = ['/select\\s.*\\sfrom/i', '/waitfor\\sdelay/i', '/pg_sleep/i']; |
| 121 | foreach ($patterns as $pattern) { |
| 122 | if (\preg_match($pattern, $string)) { |
| 123 | return \true; |
| 124 | } |
| 125 | } |
| 126 | return \false; |
| 127 | } |
| 128 | } |
| 129 |