Browse Source

Remove the php-curl dependency

pull/1474/head
Timothée Jaussoin 2 weeks ago
parent
commit
29854aadd0
  1. 1
      CHANGELOG.md
  2. 3
      INSTALL.md
  3. 10
      app/Contact.php
  4. 2
      app/Controllers/AccountController.php
  5. 2
      app/Controllers/DisconnectController.php
  6. 103
      app/Helpers/UtilsHelper.php
  7. 2
      app/Post.php
  8. 2
      app/Widgets/AccountNext/AccountNext.php
  9. 2
      app/Widgets/Login/Login.php
  10. 96
      app/Widgets/Picture/Picture.php
  11. 42
      app/Widgets/UploadFile/UploadFile.php
  12. 1
      composer.json
  13. 3
      composer.lock
  14. 4
      src/Movim/Bootstrap.php
  15. 9
      src/Movim/Console/ImportEmojisPack.php
  16. 2
      src/Movim/Controller/Front.php
  17. 9
      src/Movim/Daemon/Api.php
  18. 20
      src/Movim/Image.php

1
CHANGELOG.md

@ -36,6 +36,7 @@ v0.32 (master)
* Allow chatroom admins to configure the user voice, related to #1478
* Material Design 3 CSS changes
* Enforce Packet parameters in all the Widgets
* Drop php-curl dependency and replace it with reactphp/http
v0.31
---------------------------

3
INSTALL.md

@ -14,7 +14,6 @@ Movim requires some dependencies to be setup properly.
* MariaDB 10.2 or higher with utf8mb4 encoding (necessary for emojis 😃 support) AND `utf8mb4_bin` collation.
* MySQL is __NOT__ supported and will throw errors during the migrations, please use PostgreSQL or MariaDB.
* **PHP 8.2 minimum** with :
* Curl (package ''**php-curl**'')
* PHP mbstring (package ''**php-mbstring**'')
* PHP ImageMagick and GD for the picture processing (package ''**php-imagick**'' and ''**php-gd**'')
* Your database PHP driver (package ''**php-pgsql**'' or ''**php-mysql**'' depending on the type of database server you want to use).
@ -23,7 +22,7 @@ Movim requires some dependencies to be setup properly.
### Debian/Ubuntu
apt install composer php-fpm php-curl php-mbstring php-imagick php-gd php-pgsql php-xml php-zip
apt install composer php-fpm php-mbstring php-imagick php-gd php-pgsql php-xml php-zip
# General architecture

10
app/Contact.php

@ -108,10 +108,12 @@ class Contact extends Model
filter_var((string)$vcard->vCard->PHOTO, FILTER_VALIDATE_URL)
&& in_array($this->avatartype, ['vcard-temp', null])
) {
$this->photobin = base64_encode(
requestURL((string)$vcard->vCard->PHOTO, 1)
);
$this->avatartype = 'vcard-temp';
$data = requestURL((string)$vcard->vCard->PHOTO, timeout: 1);
if ($data) {
$this->photobin = base64_encode($data);
$this->avatartype = 'vcard-temp';
}
} elseif (
$vcard->vCard->PHOTO
&& in_array($this->avatartype, ['vcard-temp', null])

2
app/Controllers/AccountController.php

@ -12,7 +12,7 @@ class AccountController extends Base
$this->redirect('login');
}
requestAPI('disconnect', 2, ['sid' => SESSION_ID]);
requestAPI('disconnect', post: ['sid' => SESSION_ID]);
$this->page->setTitle(__('page.account_creation'));
}

2
app/Controllers/DisconnectController.php

@ -14,7 +14,7 @@ class DisconnectController extends Base
{
// Just in case
User::me()->encryptedPasswords()->delete();
requestAPI('disconnect', 2, ['sid' => SESSION_ID]);
requestAPI('disconnect', post: ['sid' => SESSION_ID]);
Session::dispose();
// Fresh cookie

103
app/Helpers/UtilsHelper.php

@ -11,6 +11,8 @@ use React\Http\Message\Response;
use React\Promise\Promise;
use React\Promise\PromiseInterface;
use function React\Async\await;
/**
* Me
*/
@ -207,7 +209,6 @@ function resolveInfos($postCollection)
function requiredExtensions(): array
{
$extensions = [
'curl',
'dom',
'imagick',
'mbstring',
@ -756,83 +757,65 @@ function requestTemplaterWorker(string $widget, string $method, ?Packet $data =
/*
* @desc Request a simple url
*/
function requestURL(string $url, int $timeout = 10, $post = false, bool $json = false, array $headers = [])
function requestURL(string $url, int $timeout = 10, bool $json = false, array $headers = []): ?string
{
if ($json) {
array_push($headers, 'Accept: application/json');
}
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
//curl_setopt($ch, CURLOPT_ENCODING, 'gzip,deflate');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_USERAGENT, DEFAULT_HTTP_USER_AGENT);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if ($post) {
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
}
$connector = null;
// Disable SSL if the host requested is the local one
if (parse_url(config('daemon.url'), PHP_URL_HOST) == parse_url($url, PHP_URL_HOST)) {
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
$connector = new React\Socket\Connector([
'tls' => [
'verify_peer' => false,
'verify_peer_name' => false
]
]);
}
$content = curl_exec($ch);
return curl_errno($ch) == 0 ? $content : false;
}
/*
* Request the headers of a URL
*/
function requestHeaders(string $url, $timeout = 2): ?array
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
//curl_setopt($ch, CURLOPT_ENCODING, 'gzip,deflate');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_NOBODY, 1);
curl_setopt($ch, CURLOPT_USERAGENT, DEFAULT_HTTP_USER_AGENT);
$ret = curl_exec($ch);
if (empty($ret)) {
\logError('requestHeader on "' . $url . '": ' . curl_error($ch));
curl_close($ch);
$browser = (new React\Http\Browser($connector))
->withTimeout($timeout)
->withHeader('User-Agent', DEFAULT_HTTP_USER_AGENT)
->withFollowRedirects(true);
try {
$response = await($browser->get($url, $headers));
// response successfully received
return (string)$response->getBody();
} catch (Exception $e) {
return null;
}
return curl_getinfo($ch);
}
/**
* Request the internal API
*/
function requestAPI(string $action, int $timeout = 2, $post = false)
{
$ch = curl_init('http:/' . $action);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_UNIX_SOCKET_PATH, API_SOCKET);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
if (is_array($post)) {
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
function requestAPI(string $action, int $timeout = 2, ?array $post = null): string|false
{
$browser = (new React\Http\Browser(
new React\Socket\FixedUriConnector(
API_SOCKET,
new React\Socket\UnixConnector()
)
))->withTimeout($timeout)
->withHeader('Content-Type', 'application/x-www-form-urlencoded')
->withHeader('Host', $action);
$url = 'http:/' . $action;
try {
$response = await(
$post
? $browser->post($url, body: http_build_query($post))
: $browser->get($url)
);
return (string)$response->getBody();
} catch (Exception $e) {
return false;
}
$content = curl_exec($ch);
return curl_errno($ch) == 0 ? $content : false;
}
/**

2
app/Post.php

@ -513,7 +513,7 @@ class Post extends Model
if ($summary != null || $content != null) {
$this->content = trim((string)$summary . (string)$content);
$this->contentcleaned = requestAPI('purifyhtml', 2, ['content' => $this->content]);
$this->contentcleaned = purifyHTML(html_entity_decode($this->content));//requestAPI('purifyhtml', post: ['content' => $this->content]);
}
$this->updated = ($entry->entry->updated)

2
app/Widgets/AccountNext/AccountNext.php

@ -81,7 +81,7 @@ class AccountNext extends \Movim\Widget\Base
{
Toast::send($this->__('error.service_unavailable'));
requestAPI('disconnect', 2, ['sid' => SESSION_ID]);
requestAPI('disconnect', post: ['sid' => SESSION_ID]);
$this->rpc('MovimUtils.redirect', $this->route('account'));
}

2
app/Widgets/Login/Login.php

@ -109,7 +109,7 @@ class Login extends Base
$this->view->assign('pop', User::count());
$this->view->assign('admins', User::where('admin', true)->get());
$this->view->assign('connected', (int)requestAPI('started', 2));
$this->view->assign('connected', (int)requestAPI('started'));
$this->view->assign('error', $this->prepareError());
if (

96
app/Widgets/Picture/Picture.php

@ -2,8 +2,11 @@
namespace App\Widgets\Picture;
use Movim\Widget\Base;
use Exception;
use Movim\Image;
use Movim\Widget\Base;
use Psr\Http\Message\ResponseInterface;
use React\Http\Browser;
class Picture extends Base
{
@ -12,7 +15,7 @@ class Picture extends Base
public function display()
{
$url = str_replace ( ' ', '%20', html_entity_decode(urldecode($this->get('url'))));
$url = str_replace(' ', '%20', html_entity_decode(urldecode($this->get('url'))));
$parsedUrl = parse_url($url);
if (
is_array($parsedUrl)
@ -22,66 +25,53 @@ class Picture extends Base
$url = getImgurThumbnail($url);
}
$headers = requestHeaders($url);
if (is_array($headers) && preg_match('/2\d{2}/', $headers['http_code'])) {
$imported = false;
$chunks = '';
$max = $headers["download_content_length"] > $this->compressLimit ? $this->compressLimit : $headers["download_content_length"];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_BUFFERSIZE, 12800);
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_setopt($ch, CURLOPT_MAXFILESIZE, $max);
curl_setopt($ch, CURLOPT_USERAGENT, DEFAULT_HTTP_USER_AGENT);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) use (&$chunks, $max) {
$chunks .= $chunk;
return strlen($chunk);
});
curl_exec($ch);
curl_close($ch);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = preg_split('/[\r\n]+/', substr($chunks, 0, $headerSize));
$body = substr($chunks, $headerSize);
$p = null;
$browser = (new Browser())
->withHeader('User-Agent', DEFAULT_HTTP_USER_AGENT)
->withFollowRedirects(true);
$p = new Image;
$browser->withTimeout(2)->head($url)->then(function (ResponseInterface $response) use ($url, $browser) {
$contentLength = null;
/**
* In case of an animated GIF we get only the first frame
*/
if (substr_count($chunks, "\x00\x21\xF9\x04") > 1) {
$body = substr($body, 0, strrpos($body, "\x00\x21\xF9\x04"));
if ($response->hasHeader('Content-Length')) {
$contentLength = (int)$response->getHeader('Content-Length')[0];
}
$imported = $p->fromBin($body);
$max = $contentLength && $contentLength > $this->compressLimit
? $this->compressLimit
: $contentLength;
if ($imported) {
$p->inMemory();
$p->save(quality: 85);
$browser->withTimeout(10)->withResponseBuffer($max)->get($url)->then(function (ResponseInterface $response) {
$imported = false;
$body = (string)$response->getBody();
$p = new Image;
if ($p->getImage()->getImageWidth() > $this->sizeLimit || $p->getImage()->getImageHeight() > $this->sizeLimit) {
$p->getImage()->adaptiveResizeImage($this->sizeLimit, $this->sizeLimit, true, false);
// In case of an animated GIF we get only the first frame
if (substr_count($body, "\x00\x21\xF9\x04") > 1) {
$body = substr($body, 0, strrpos($body, "\x00\x21\xF9\x04"));
}
header_remove('Content-Type');
header('Content-Type: image/' . DEFAULT_PICTURE_FORMAT);
header('Cache-Control: max-age=' . 3600 * 24);
print $p ? $p->getImage()->getImagesBlob() : $body;
$imported = $p->fromBin($body);
return;
}
}
if ($imported) {
$p->inMemory();
$p->save(quality: 85);
if ($p->getImage()->getImageWidth() > $this->sizeLimit || $p->getImage()->getImageHeight() > $this->sizeLimit) {
$p->getImage()->adaptiveResizeImage($this->sizeLimit, $this->sizeLimit, true, false);
}
header_remove('Content-Type');
header('Content-Type: image/' . DEFAULT_PICTURE_FORMAT);
header('Cache-Control: max-age=' . 3600 * 24);
print $p ? $p->getImage()->getImagesBlob() : $body;
return;
}
});
header("HTTP/1.1 301 Moved Permanently");
header('Location: /theme/img/broken_image_filled.svg');
}, function (Exception $e) {
header("HTTP/1.1 301 Moved Permanently");
header('Location: /theme/img/broken_image_filled.svg');
});
}
}

42
app/Widgets/UploadFile/UploadFile.php

@ -4,6 +4,8 @@ namespace App\Widgets\UploadFile;
use App\Upload;
use Movim\Widget\Base;
use Psr\Http\Message\ResponseInterface;
use React\Http\Browser;
class UploadFile extends Base
{
@ -18,39 +20,19 @@ class UploadFile extends Base
if (!$upload) return;
if (array_key_exists('file', $_FILES)) {
$ch = curl_init($upload->puturl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_PUT, true);
curl_setopt($ch,CURLOPT_TIMEOUT,10);
$tmpFile = fopen($_FILES['file']['tmp_name'], 'r');
curl_setopt($ch, CURLOPT_INFILE, $tmpFile);
curl_setopt($ch, CURLOPT_INFILESIZE, filesize($_FILES['file']['tmp_name']));
if (is_array($upload->headers)) {
$formatedHeaders = [];
foreach ($upload->headers as $key => $value) {
array_push ($formatedHeaders, $key . ': ' . $value);
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $formatedHeaders);
}
//curl_setopt($ch, CURLOPT_NOPROGRESS, false);
//curl_setopt($ch, CURLOPT_BUFFERSIZE, 128);
//curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, [$this, 'progressCallback']);
curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode >= 200 && $httpCode < 300) {
$browser = (new Browser())->withTimeout(10)
->withFollowRedirects(true);
$browser->put(
$upload->puturl,
is_array($upload->headers) ? $upload->headers : [],
file_get_contents($_FILES['file']['tmp_name'])
)->then(function (ResponseInterface $response) use ($upload) {
$upload->uploaded = true;
$upload->save();
}
http_response_code($httpCode);
http_response_code($response->getStatusCode());
});
}
}
}

1
composer.json

@ -13,7 +13,6 @@
},
"require": {
"php": ">=8.2.0",
"ext-curl": "*",
"ext-gd": "*",
"ext-dom": "*",
"ext-mbstring": "*",

3
composer.lock

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "a68eb663f09739bf9b9964c15bbcbded",
"content-hash": "682142e59af45053576467f71f62636f",
"packages": [
{
"name": "awobaz/compoships",
@ -7480,7 +7480,6 @@
"prefer-lowest": false,
"platform": {
"php": ">=8.2.0",
"ext-curl": "*",
"ext-gd": "*",
"ext-dom": "*",
"ext-mbstring": "*",

4
src/Movim/Bootstrap.php

@ -222,7 +222,7 @@ class Bootstrap
private function checkSession()
{
if (is_string(SESSION_ID)) {
$process = (bool)requestAPI('exists', 2, ['sid' => SESSION_ID]);
$process = (bool)requestAPI('exists', post: ['sid' => SESSION_ID]);
$session = DBSession::find(SESSION_ID);
if ($session) {
@ -237,7 +237,7 @@ class Bootstrap
$session->loadMemory();
} elseif ($process) {
// A process but no session in the db
requestAPI('disconnect', 2, ['sid' => SESSION_ID]);
requestAPI('disconnect', post: ['sid' => SESSION_ID]);
}
}
}

9
src/Movim/Console/ImportEmojisPack.php

@ -50,7 +50,7 @@ class ImportEmojisPack extends Command
$output->writeln('<info>Downloading the manifest</info>');
$content = requestURL($input->getArgument('manifest-url'), 5, false, true);
$content = requestURL($input->getArgument('manifest-url'), timeout: 5, json: true);
if (!$content) {
$output->writeln('<error>The manifest cannot be downloaded</error>');
@ -85,7 +85,12 @@ class ImportEmojisPack extends Command
$output->writeln('<info>Downloading ' . $pack . ' - ' . $json->{$pack}->description . '</info>');
$content = requestURL($json->{$pack}->src, 5);
$content = requestURL($json->{$pack}->src, timeout: 5);
if (!$content) {
$output->writeln('<error>The archive cannot be downloaded</error>');
return Command::FAILURE;
}
$tempZip = tempnam(sys_get_temp_dir(), $pack);
file_put_contents($tempZip, $content);

2
src/Movim/Controller/Front.php

@ -61,7 +61,7 @@ class Front extends Base
// Ajax request that is going to the daemon
if ($request === 'ajaxd') {
requestAPI('ajax', 2, [
requestAPI('ajax', post: [
'sid' => SESSION_ID,
'json' => rawurlencode(file_get_contents('php://input'))
]);

9
src/Movim/Daemon/Api.php

@ -48,10 +48,6 @@ class Api
case 'session':
$response = $api->getSession();
break;
case 'purifyhtml':
$response = $api->purifyHtml($request->getParsedBody());
break;
}
return new Response(
@ -84,11 +80,6 @@ class Api
&& $sessions[$sid] == true);
}
public function purifyHtml($post)
{
return purifyHTML(html_entity_decode($post['content']));
}
public function sessionsLinked()
{
return count($this->_core->getSessions());

20
src/Movim/Image.php

@ -72,12 +72,14 @@ class Image
if ($this->_im) {
return $this->_im->getImageGeometry();
}
return null;
}
/**
* @desc Load a bin picture from an URL
*/
public function fromURL(string $url):bool
public function fromURL(string $url): bool
{
$bin = requestURL($url);
if ($bin) {
@ -204,12 +206,14 @@ class Image
// And destroy all the thumbnails if it's the original
if ($width == false) {
foreach (glob(
PUBLIC_CACHE_PATH .
hash(Image::$hash, $this->_key) .
'*' . self::$formats[$format],
GLOB_NOSORT
) as $pathThumb) {
foreach (
glob(
PUBLIC_CACHE_PATH .
hash(Image::$hash, $this->_key) .
'*' . self::$formats[$format],
GLOB_NOSORT
) as $pathThumb
) {
@unlink($pathThumb);
}
}
@ -250,7 +254,7 @@ class Image
}
// Auto-rotate
switch($this->_im->getImageOrientation()) {
switch ($this->_im->getImageOrientation()) {
case \Imagick::ORIENTATION_BOTTOMRIGHT:
$this->_im->rotateimage("#000", 180);
break;

Loading…
Cancel
Save