397 lines
15 KiB
PHP
397 lines
15 KiB
PHP
<?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);
|
||
}
|
||
}
|
||
}
|