2019-10-11 07:38:42 +02:00
< ? php
/**
2023-01-01 09:36:24 -05:00
* @ copyright Copyright ( C ) 2010 - 2023 , the Friendica project
2020-02-09 16:18:46 +01:00
*
2022-01-07 00:13:00 +01:00
* @ license GNU AGPL version 3 or any later version
2020-02-09 16:18:46 +01:00
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https :// www . gnu . org / licenses />.
*
2019-10-11 07:38:42 +02:00
*/
namespace Friendica\Module\Settings ;
2022-10-17 10:37:48 +00:00
use Friendica\App ;
2019-10-11 07:38:42 +02:00
use Friendica\Core\Hook ;
2022-10-24 17:56:05 -04:00
use Friendica\Core\L10n ;
2019-10-11 07:38:42 +02:00
use Friendica\Core\Renderer ;
2022-10-24 17:56:05 -04:00
use Friendica\Core\Session\Capability\IHandleUserSessions ;
2022-05-18 02:13:54 +00:00
use Friendica\Core\System ;
2019-10-11 07:38:42 +02:00
use Friendica\Database\DBA ;
2022-10-24 17:56:05 -04:00
use Friendica\Database\Definition\DbaDefinition ;
2019-12-15 22:34:11 +01:00
use Friendica\DI ;
2022-10-24 17:56:05 -04:00
use Friendica\Model\Contact ;
2020-09-27 10:24:15 +00:00
use Friendica\Model\Item ;
2021-01-19 07:23:01 +00:00
use Friendica\Model\Post ;
2020-01-22 23:14:14 -05:00
use Friendica\Module\BaseSettings ;
2022-10-24 17:56:05 -04:00
use Friendica\Module\Response ;
2021-04-01 19:29:21 -04:00
use Friendica\Network\HTTPException ;
2022-10-24 17:56:05 -04:00
use Friendica\Network\HTTPException\ForbiddenException ;
use Friendica\Network\HTTPException\InternalServerErrorException ;
use Friendica\Network\HTTPException\ServiceUnavailableException ;
use Friendica\Util\Profiler ;
use Psr\Log\LoggerInterface ;
2019-10-11 07:38:42 +02:00
/**
* Module to export user data
**/
2020-01-22 23:14:14 -05:00
class UserExport extends BaseSettings
2019-10-11 07:38:42 +02:00
{
2022-10-24 17:56:05 -04:00
/** @var DbaDefinition */
private $dbaDefinition ;
2022-11-12 08:37:42 -05:00
public function __construct ( DbaDefinition $dbaDefinition , IHandleUserSessions $session , App\Page $page , L10n $l10n , App\BaseURL $baseUrl , App\Arguments $args , LoggerInterface $logger , Profiler $profiler , Response $response , array $server , array $parameters = [])
2022-10-24 17:56:05 -04:00
{
2022-11-12 08:37:42 -05:00
parent :: __construct ( $session , $page , $l10n , $baseUrl , $args , $logger , $profiler , $response , $server , $parameters );
2022-10-24 17:56:05 -04:00
$this -> dbaDefinition = $dbaDefinition ;
}
2019-10-11 07:38:42 +02:00
/**
* Handle the request to export data .
2019-11-03 00:12:16 +01:00
* At the moment one can export three different data set
2019-10-11 07:38:42 +02:00
* 1. The profile data that can be used by uimport to resettle
* to a different Friendica instance
* 2. The entire data - set , profile plus postings
2019-11-03 00:12:16 +01:00
* 3. A list of contacts as CSV file similar to the export of Mastodon
2019-10-11 11:49:28 +02:00
*
* If there is an action required through the URL / path , react
* accordingly and export the requested data .
2021-04-01 19:29:21 -04:00
*
2022-10-24 17:56:05 -04:00
* @ param array $request
2021-04-01 19:29:21 -04:00
* @ return string
2022-10-24 17:56:05 -04:00
* @ throws ForbiddenException
* @ throws InternalServerErrorException
* @ throws ServiceUnavailableException
2021-04-01 19:29:21 -04:00
*/
2021-11-20 15:38:03 +01:00
protected function content ( array $request = []) : string
2019-10-11 07:38:42 +02:00
{
2022-10-24 17:56:05 -04:00
if ( ! $this -> session -> getLocalUserId ()) {
throw new HTTPException\ForbiddenException ( $this -> l10n -> t ( 'Permission denied.' ));
2021-04-01 19:29:21 -04:00
}
2021-11-14 20:46:25 +01:00
parent :: content ();
2019-10-17 07:45:48 +02:00
2019-10-11 07:38:42 +02:00
/**
* options shown on " Export personal data " page
* list of array ( 'link url' , 'link text' , 'help text' )
*/
$options = [
2022-10-24 17:56:05 -04:00
[ 'settings/userexport/account' , $this -> l10n -> t ( 'Export account' ), $this -> l10n -> t ( 'Export your account info and contacts. Use this to make a backup of your account and/or to move it to another server.' )],
[ 'settings/userexport/backup' , $this -> l10n -> t ( 'Export all' ), $this -> l10n -> t ( 'Export your account info, contacts and all your items as json. Could be a very big file, and could take a lot of time. Use this to make a full backup of your account (photos are not exported)' )],
[ 'settings/userexport/contact' , $this -> l10n -> t ( 'Export Contacts to CSV' ), $this -> l10n -> t ( 'Export the list of the accounts you are following as CSV file. Compatible to e.g. Mastodon.' )],
2019-10-11 07:38:42 +02:00
];
Hook :: callAll ( 'uexport_options' , $options );
2022-10-24 17:56:05 -04:00
$tpl = Renderer :: getMarkupTemplate ( 'settings/userexport.tpl' );
2019-10-11 07:38:42 +02:00
return Renderer :: replaceMacros ( $tpl , [
2022-10-24 17:56:05 -04:00
'$title' => $this -> l10n -> t ( 'Export personal data' ),
2019-10-11 07:38:42 +02:00
'$options' => $options
]);
}
2021-04-01 19:29:21 -04:00
2019-10-17 07:45:48 +02:00
/**
* raw content generated for the different choices made
* by the user . At the moment this returns a JSON file
* to the browser which then offers a save / open dialog
* to the user .
2021-04-01 19:29:21 -04:00
*
* @ throws HTTPException\ForbiddenException
*/
2021-11-20 15:38:03 +01:00
protected function rawContent ( array $request = [])
2019-11-02 11:24:46 +01:00
{
2022-10-24 17:56:05 -04:00
if ( ! $this -> session -> getLocalUserId ()) {
throw new HTTPException\ForbiddenException ( $this -> l10n -> t ( 'Permission denied.' ));
2021-04-01 19:29:21 -04:00
}
2022-10-24 17:56:05 -04:00
if ( isset ( $this -> parameters [ 'action' ])) {
switch ( $this -> parameters [ 'action' ]) {
case 'backup' :
header ( 'Content-type: application/json' );
header ( 'Content-Disposition: attachment; filename="' . DI :: app () -> getLoggedInUserNickname () . '.' . $this -> parameters [ 'action' ] . '"' );
$this -> echoAll ( $this -> session -> getLocalUserId ());
2019-10-17 07:45:48 +02:00
break ;
2022-10-24 17:56:05 -04:00
case 'account' :
header ( 'Content-type: application/json' );
header ( 'Content-Disposition: attachment; filename="' . DI :: app () -> getLoggedInUserNickname () . '.' . $this -> parameters [ 'action' ] . '"' );
$this -> echoAccount ( $this -> session -> getLocalUserId ());
2019-10-17 07:45:48 +02:00
break ;
2022-10-24 17:56:05 -04:00
case 'contact' :
header ( 'Content-type: application/csv' );
2021-08-09 19:48:39 +00:00
header ( 'Content-Disposition: attachment; filename="' . DI :: app () -> getLoggedInUserNickname () . '-contacts.csv' . '"' );
2022-10-24 17:56:05 -04:00
$this -> echoContactsAsCSV ( $this -> session -> getLocalUserId ());
2019-11-03 00:12:16 +01:00
break ;
2019-10-17 07:45:48 +02:00
}
2022-05-18 02:13:54 +00:00
System :: exit ();
2019-10-17 07:45:48 +02:00
}
}
2021-04-01 19:29:21 -04:00
/**
* @ param string $query
* @ return array
* @ throws \Exception
*/
2022-10-24 17:56:05 -04:00
private function exportMultiRow ( string $query ) : array
2019-11-02 11:24:46 +01:00
{
2022-10-24 17:56:05 -04:00
$dbStructure = $this -> dbaDefinition -> getAll ();
2019-10-11 07:38:42 +02:00
2022-10-24 17:56:05 -04:00
preg_match ( '/\s+from\s+`?([a-z\d_]+)`?/i' , $query , $match );
2019-10-11 07:38:42 +02:00
$table = $match [ 1 ];
$result = [];
2019-11-02 12:12:29 +01:00
$rows = DBA :: p ( $query );
while ( $row = DBA :: fetch ( $rows )) {
$p = [];
2020-08-20 09:30:50 -04:00
foreach ( $dbStructure [ $table ][ 'fields' ] as $column => $field ) {
2020-09-27 10:24:15 +00:00
if ( ! isset ( $row [ $column ])) {
continue ;
}
2020-08-20 09:30:50 -04:00
if ( $field [ 'type' ] == 'datetime' ) {
2020-09-27 10:24:15 +00:00
$p [ $column ] = $row [ $column ] ? ? DBA :: NULL_DATETIME ;
2020-08-20 09:30:50 -04:00
} else {
2020-09-27 10:24:15 +00:00
$p [ $column ] = $row [ $column ];
2019-10-11 07:38:42 +02:00
}
}
2019-11-02 12:12:29 +01:00
$result [] = $p ;
2019-10-11 07:38:42 +02:00
}
2019-11-02 12:12:29 +01:00
DBA :: close ( $rows );
2019-10-11 07:38:42 +02:00
return $result ;
}
2021-04-01 19:29:21 -04:00
/**
* @ param string $query
* @ return array
* @ throws \Exception
*/
2022-10-24 17:56:05 -04:00
private function exportRow ( string $query ) : array
2019-11-02 11:24:46 +01:00
{
2022-10-24 17:56:05 -04:00
$dbStructure = $this -> dbaDefinition -> getAll ();
2019-10-11 07:38:42 +02:00
2022-10-24 17:56:05 -04:00
preg_match ( '/\s+from\s+`?([a-z\d_]+)`?/i' , $query , $match );
2019-10-11 07:38:42 +02:00
$table = $match [ 1 ];
$result = [];
2021-10-08 04:10:45 +00:00
$rows = DBA :: p ( $query );
while ( $row = DBA :: fetch ( $rows )) {
foreach ( $row as $k => $v ) {
if ( empty ( $dbStructure [ $table ][ 'fields' ][ $k ])) {
continue ;
}
switch ( $dbStructure [ $table ][ 'fields' ][ $k ][ 'type' ]) {
case 'datetime' :
$result [ $k ] = $v ? ? DBA :: NULL_DATETIME ;
break ;
default :
$result [ $k ] = $v ;
break ;
2019-10-11 07:38:42 +02:00
}
}
}
2021-10-08 04:10:45 +00:00
DBA :: close ( $rows );
2021-04-01 19:29:21 -04:00
2019-10-11 07:38:42 +02:00
return $result ;
}
2019-11-03 00:12:16 +01:00
/**
* Export a list of the contacts as CSV file as e . g . Mastodon and Pleroma are doing .
2021-04-01 19:29:21 -04:00
*
* @ param int $user_id
* @ throws \Exception
*/
2022-10-24 17:56:05 -04:00
private function echoContactsAsCSV ( int $user_id )
2019-11-03 00:12:16 +01:00
{
2021-04-01 19:29:21 -04:00
if ( ! $user_id ) {
2022-10-24 17:56:05 -04:00
throw new \RuntimeException ( $this -> l10n -> t ( 'Permission denied.' ));
2021-04-01 19:29:21 -04:00
}
2019-11-03 00:12:16 +01:00
// write the table header (like Mastodon)
echo " Account address, Show boosts \n " ;
// get all the contacts
2022-10-24 17:56:44 -04:00
$contacts = DBA :: select ( 'contact' , [ 'addr' , 'url' ], [ 'uid' => $user_id , 'self' => false , 'rel' => [ Contact :: SHARING , Contact :: FRIEND ], 'deleted' => false , 'archive' => false ]);
2019-11-03 14:03:11 +01:00
while ( $contact = DBA :: fetch ( $contacts )) {
2020-09-27 14:08:41 +00:00
echo ( $contact [ 'addr' ] ? : $contact [ 'url' ]) . " , true \n " ;
2019-11-03 00:12:16 +01:00
}
2019-11-03 14:03:11 +01:00
DBA :: close ( $contacts );
2019-11-03 00:12:16 +01:00
}
2021-04-01 19:29:21 -04:00
/**
* @ param int $user_id
* @ throws \Exception
*/
2022-10-24 17:56:05 -04:00
private function echoAccount ( int $user_id )
2019-11-02 11:24:46 +01:00
{
2021-04-01 19:29:21 -04:00
if ( ! $user_id ) {
2022-10-24 17:56:05 -04:00
throw new \RuntimeException ( $this -> l10n -> t ( 'Permission denied.' ));
2021-04-01 19:29:21 -04:00
}
2022-10-24 17:56:05 -04:00
$user = $this -> exportRow (
2021-04-01 19:29:21 -04:00
sprintf ( " SELECT * FROM `user` WHERE `uid` = %d LIMIT 1 " , $user_id )
2019-10-11 07:38:42 +02:00
);
2022-10-24 17:56:05 -04:00
$contact = $this -> exportMultiRow (
2021-04-01 19:29:21 -04:00
sprintf ( " SELECT * FROM `contact` WHERE `uid` = %d " , $user_id )
2019-10-11 07:38:42 +02:00
);
2022-10-24 17:56:05 -04:00
$profile = $this -> exportMultiRow (
2021-04-01 19:29:21 -04:00
sprintf ( " SELECT *, 'default' AS `profile_name`, 1 AS `is-default` FROM `profile` WHERE `uid` = %d " , $user_id )
2020-01-22 19:36:20 -05:00
);
2022-10-24 17:56:05 -04:00
$profile_fields = $this -> exportMultiRow (
2021-04-01 19:29:21 -04:00
sprintf ( " SELECT * FROM `profile_field` WHERE `uid` = %d " , $user_id )
2019-10-11 07:38:42 +02:00
);
2022-10-24 17:56:05 -04:00
$photo = $this -> exportMultiRow (
2021-04-01 19:29:21 -04:00
sprintf ( " SELECT * FROM `photo` WHERE uid = %d AND profile = 1 " , $user_id )
2019-10-11 07:38:42 +02:00
);
foreach ( $photo as & $p ) {
$p [ 'data' ] = bin2hex ( $p [ 'data' ]);
}
2022-10-24 17:56:05 -04:00
$pconfig = $this -> exportMultiRow (
2021-04-01 19:29:21 -04:00
sprintf ( " SELECT * FROM `pconfig` WHERE uid = %d " , $user_id )
2019-10-11 07:38:42 +02:00
);
2023-05-13 19:54:35 -04:00
$circle = $this -> exportMultiRow (
2021-04-01 19:29:21 -04:00
sprintf ( " SELECT * FROM `group` WHERE uid = %d " , $user_id )
2019-10-11 07:38:42 +02:00
);
2023-05-13 19:54:35 -04:00
$circle_member = $this -> exportMultiRow (
sprintf ( " SELECT `circle_member`.`gid`, `circle_member`.`contact-id` FROM `group_member` AS `circle_member` INNER JOIN `group` AS `circle` ON `circle`.`id` = `circle_member`.`gid` WHERE `circle`.`uid` = %d " , $user_id )
2019-10-11 07:38:42 +02:00
);
$output = [
2022-10-17 10:37:48 +00:00
'version' => App :: VERSION ,
2019-10-11 07:38:42 +02:00
'schema' => DB_UPDATE_VERSION ,
2022-10-24 17:56:05 -04:00
'baseurl' => $this -> baseUrl ,
2019-10-11 07:38:42 +02:00
'user' => $user ,
'contact' => $contact ,
'profile' => $profile ,
2020-01-22 19:36:20 -05:00
'profile_fields' => $profile_fields ,
2019-10-11 07:38:42 +02:00
'photo' => $photo ,
'pconfig' => $pconfig ,
2023-05-13 19:54:35 -04:00
'circle' => $circle ,
'circle_member' => $circle_member ,
2019-10-11 07:38:42 +02:00
];
echo json_encode ( $output , JSON_PARTIAL_OUTPUT_ON_ERROR );
}
/**
* echoes account data and items as separated json , one per line
*
2021-04-01 19:29:21 -04:00
* @ param int $user_id
2020-01-19 16:29:55 +01:00
* @ throws \Exception
2019-10-11 07:38:42 +02:00
*/
2022-10-24 17:56:05 -04:00
private function echoAll ( int $user_id )
2019-11-02 11:24:46 +01:00
{
2021-04-01 19:29:21 -04:00
if ( ! $user_id ) {
2022-10-24 17:56:05 -04:00
throw new \RuntimeException ( $this -> l10n -> t ( 'Permission denied.' ));
2021-04-01 19:29:21 -04:00
}
2022-10-24 17:56:05 -04:00
$this -> echoAccount ( $user_id );
2019-10-11 07:38:42 +02:00
echo " \n " ;
2021-04-01 19:29:21 -04:00
$total = Post :: count ([ 'uid' => $user_id ]);
2019-10-11 07:38:42 +02:00
// chunk the output to avoid exhausting memory
for ( $x = 0 ; $x < $total ; $x += 500 ) {
2021-04-01 19:29:21 -04:00
$items = Post :: selectToArray ( Item :: ITEM_FIELDLIST , [ 'uid' => $user_id ], [ 'limit' => [ $x , 500 ]]);
2020-09-27 10:24:15 +00:00
$output = [ 'item' => $items ];
2021-04-01 19:29:21 -04:00
echo json_encode ( $output , JSON_PARTIAL_OUTPUT_ON_ERROR ) . " \n " ;
2019-10-11 07:38:42 +02:00
}
}
}