accounts/hood/accounts/system/vk.php

397 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace hood\accounts;
use hood\accounts\auth\basic;
use GuzzleHttp\Client as browser,
GuzzleHttp\Cookie\FileCookieJar,
GuzzleHttp\TransferStats;
use DOMDocument,
DOMXPath;
use Exception;
/**
* Попка
*
* @todo
* 1. Вернуть внутреннее хранение cookies, а выгрузку в файл сделать отдельным методом: "dump();".
* $this->cookies - строка cookie, $this->root_path - корневая директория (которая сейчас $this->path), $this->cookies_path - путь до файла хранящего cookies
* 2. Сделать возможность авторизации без входного и пароля, указав место хранения файла cookies
* 4. Добавить возможность авторизации через сторонний браузер, который более походит на настоящий (низкий приоритет)
* 5. Создать debug-режим в котором будут сохранены обрабатываемые html страницы и действия будут записываться по PSR-7 в журнал (низкий приоритет)
*/
final class vk extends account implements basic
{
/**
* @var int $id Идентификатор
*/
private int $id;
/**
* @var string $login Входной [псевдоним]
*/
private string $login;
/**
* @var string $password Пароль
*/
private string $password;
/**
* @var string $key Ключ доступа
*/
private string $key;
/**
* Конструктор
*
* @param int $id Идентификатор
* @param string|null $path Корневой каталог аккаунтов
*
* @return self
*/
public function __construct(int $id, string $path = null)
{
// Идентификатор
$this->id = $id;
// Инициализация директории пользователя
if (isset($path)) {
// Если передан путь и он существует
$this->path = $path . DIRECTORY_SEPARATOR . $id;
} else {
// Иначе путь по умолчанию
$this->path = __DIR__ . DIRECTORY_SEPARATOR . 'accounts' . DIRECTORY_SEPARATOR . $id;
}
// Проверка и создание директории
if (!file_exists($this->path)) {
mkdir($this->path, 0775, true);
}
// Инициализация браузера
$this->browser();
}
/**
* Аутентификация
*
* @param string $login Входной
* @param string $password Пароль
* @param int $mode Режим
*
* @return self
*
* @todo
* 1. Добавить проверку требования двухэтапной аутентификации
* 2. Добавить проверку требования ввода капчи
* 3. Добавить проверку неудачного ввода пароля
* 4. Добавить аутентификацию через версию для ПК
* 5. Добавить идентификацию капчи, решение капчи и тесты с капчей
*/
public function auth(string $login, string $password, int $mode = 0): self
{
if (isset($this->login, $this->password)) {
throw new Exception('Повторная аутентификация запрещена');
}
// Инициализация свойств
$this->login = $login;
$this->password = $password;
// Переход на страницу аутентификации и обработка формы
if ($mode === 0) {
// Если установлен режим мобильной версии (по умолчанию)
// Запрос страницы с аутентификацией
$response = $this->browser->request('GET', 'https://m.vk.com');
// Проверка
$body = $this->check((string) $response->getBody());
if ($response->getStatusCode() === 200) {
// Инициализация DOM
$dom = new DOMDocument;
@$dom->loadHTML($body);
// Ссылка для отправки формы (аутентификация)
$action = $dom->getElementsByTagName('form')[0]->getAttribute('action');
} else {
throw new Exception('Не удалось получить страницу аутентификации: ' . $response->getReasonPhrase(), $response->getStatusCode());
}
} else if ($mode === 1) {
// Иначе, если установлен режим аутентификации через обычную версию
// $this->browser->post('http://login.vk.com/?act=login');
}
// Аутентификация
$response = $this->browser->request(
'POST',
$action,
[
'form_params' => [
'email' => $login,
'pass' => $password
]
]
);
// Поиск уведомления с ошибкой
$warning = $this->xpath((string) $response->getBody(), "//div[contains(@class, 'service_msg_box')]/div[contains(@class, 'service_msg service_msg_warning')]/text()");
if (!empty($warning[0]->textContent)) {
// Если аутентификация не прошла и появилось окно с ошибкой
throw new Exception('ВКонтакте: "' . trim($warning[0]->textContent) . '"');
}
return $this;
}
/**
* @todo Сделать
*/
public function deauth(): self
{
// Очистка cookie
if (file_exists($this->path . DIRECTORY_SEPARATOR . 'cookie.txt')) {
// Если сущестуют cookie, то удалить
unlink($this->path . DIRECTORY_SEPARATOR . 'cookie.txt');
// Ренициализация браузера
$this->browser();
}
return $this;
}
public function key(int $project_id = null, string ...$rights): string
{
if (!is_null($project_id)) {
// Если переданы параметры
// Инициализация
$uri = '';
// Запрос и обработка страницы подтверждения генерации ключа
$response = $this->browser->request(
'POST',
'https://oauth.vk.com/authorize?client_id=' . $project_id . '&redirect_uri=https://oauth.vk.com/blank.html&display=mobile&scope=' . (empty($rights) ? 140488159 : implode(',', $rights)) . '&response_type=token',
[
'http_errors' => false,
'on_stats' => function (TransferStats $stats) use (&$uri) {
$uri = $stats->getEffectiveUri();
}
]
);
// Проверка
$body = $this->check((string) $response->getBody());
if ($response->getStatusCode() === 200) {
// Поиск текста
$text = $this->xpath($body, "/html/body/text()|/html/body/b/text()");
// Инкрементация найденных строк в одну
for ($body = '', $i = 1; $i < count($text); $body .= $text[$i++]->textContent);
// Обрезка переносов строки и пробелов
$body = trim($body);
if ($body !== "Пожалуйста, не копируйте данные из адресной строки для сторонних сайтов. Таким образом Вы можете потерять доступ к Вашему аккаунту.") {
// Если показывает страницу подтверждения генерации токена (после генерации подтверждать не просит и сразу выдаёт токен)
// Инициализация DOM
$dom = new DOMDocument;
@$dom->loadHTML((string) $response->getBody());
// Ссылка для отправки формы (подтверждение выдачи ключа)
$action = $dom->getElementsByTagName('form')[0]->getAttribute('action');
// Запрос ключа
$response = $this->browser->request(
'POST',
$action,
[
'http_errors' => false,
'on_stats' => function (TransferStats $stats) use (&$uri) {
$uri = $stats->getEffectiveUri();
}
]
);
// Проверка ответа на наличие json с ошибкой
$this->check((string) $response->getBody());
}
// Извлечение ключа из URI
$parts = parse_url((string) $uri);
parse_str($parts['fragment'], $fragments);
// Запись ключа
$this->key = $fragments['access_token'];
} else {
throw new Exception('ВКонтакте: "' . json_decode((string) $response->getBody())->error_description . '"');
}
}
return $this->key;
}
private function browser(): browser
{
return $this->browser = new browser([
'verify' => $this->ssl,
'cookies' => (new FileCookieJar($this->path . DIRECTORY_SEPARATOR . 'cookie.txt'))
]);
}
private function xpath(string $html, string $query): ?object
{
// DOM
$dom = new DOMDocument;
@$dom->loadHTML($html);
// XPATH 1.0
$xpath = new DOMXPath($dom);
return $xpath->query($query);
}
private function check(string $response): ?string
{
$json = json_decode($response);
if (json_last_error() === JSON_ERROR_NONE) {
// Если это JSON
if (isset($json->error)) {
// Если есть ошибки
throw new Exception('ВКонтакте: "' . ($json->error['error_msg'] ?? $json->error_description) . '"', $json->error['error_code'] ?? 0);
}
}
return $response;
}
/**
* Магический метод: установить свойство
*
* @param mixed $name Название
* @param mixed $value Значение
*
* @return void
*/
public function __set($name, $value): void
{
if ($name === 'id') {
throw new Exception('Запрещено инициализировать идентификатор');
} else if ($name === 'login' || $name === 'name' || $name === 'email' || $name === 'phone' || $name === 'number' || $name === 'nick' || $name === 'nickname') {
throw new Exception('Запрещено инициализировать входной');
} else if ($name === 'password' || $name === 'pswd' || $name === 'pass') {
throw new Exception('Запрещено инициализировать пароль');
} else if ($name === 'key' || $name === 'token') {
$this->key = $value;
} else if ($name === 'browser') {
throw new Exception('Запрещено инициализировать браузер');
} else if ($name === 'path') {
$this->path = $value. DIRECTORY_SEPARATOR . $this->id;
// Проверка и создание директории
if (!file_exists($this->path)) {
mkdir($this->path, 0775, true);
}
// Реинициализация браузера с новым значением
$this->browser();
} else if ($name === 'ssl') {
$this->ssl = $value;
// Реинициализация браузера с новым значением
$this->browser();
}
}
/**
* Магический метод: получить свойство
*
* @param mixed $name Название
*
* @return mixed
*/
public function __get($name)
{
if ($name === 'id') {
return $this->id;
} else if ($name === 'login' || $name === 'name' || $name === 'email' || $name === 'phone' || $name === 'number' || $name === 'nick' || $name === 'nickname') {
return $this->login;
} else if ($name === 'password' || $name === 'pswd' || $name === 'pass') {
return $this->password;
} else if ($name === 'key') {
return $this->key;
} else if ($name === 'browser') {
return $this->browser;
} else if ($name === 'path') {
return $this->path;
} else if ($name === 'ssl') {
return $this->ssl ?? $this->ssl = true;
}
}
/**
* Магический метод: проверка на инициализированность
*
* @param mixed $name Название
*
* @return mixed
*/
public function __isset($name)
{
if ($name === 'id') {
return isset($this->id);
} else if ($name === 'login' || $name === 'name' || $name === 'email' || $name === 'phone' || $name === 'number' || $name === 'nick' || $name === 'nickname') {
return isset($this->login);
} else if ($name === 'password' || $name === 'pswd' || $name === 'pass') {
return isset($this->password);
} else if ($name === 'key') {
return isset($this->key);
} else if ($name === 'browser') {
return isset($this->browser);
} else if ($name === 'path') {
return isset($this->path);
} else if ($name === 'ssl') {
return isset($this->ssl);
}
}
/**
* Магический метод: удаление
*
* @param mixed $name Название
*
* @return mixed
*/
public function __unset($name)
{
if ($name === 'id') {
throw new Exception('Запрещено деинициализировать идентификатор');
} else if ($name === 'login' || $name === 'name' || $name === 'email' || $name === 'phone' || $name === 'number' || $name === 'nick' || $name === 'nickname') {
throw new Exception('Запрещено деинициализировать входной');
} else if ($name === 'password' || $name === 'pswd' || $name === 'pass') {
throw new Exception('Запрещено деинициализировать пароль');
} else if ($name === 'key') {
unset($this->key);
} else if ($name === 'browser') {
unset($this->browser);
} else if ($name === 'path') {
unset($this->path);
} else if ($name === 'ssl') {
unset($this->ssl);
}
}
}