Remove addon repo management from admin/addons.

This functionality was broken anyways, and needs to be rethought. Addon
reporitories can still be managed from the terminal using the tools in
util/.
This commit is contained in:
Harald Eilertsen
2024-10-09 20:51:00 +02:00
parent 44232677c8
commit 6588e272db
4 changed files with 3 additions and 613 deletions

View File

@@ -4,7 +4,6 @@ namespace Zotlabs\Module\Admin;
use App;
use \Zotlabs\Lib\Config;
use \Zotlabs\Storage\GitRepo;
use \Michelf\MarkdownExtra;
class Addons {
@@ -24,227 +23,6 @@ class Addons {
goaway(z_root() . '/admin/addons/' . argv(2) );
}
elseif(argc() > 2) {
switch(argv(2)) {
case 'updaterepo':
if (array_key_exists('repoName', $_REQUEST)) {
$repoName = $_REQUEST['repoName'];
}
else {
json_return_and_die(array('message' => 'No repo name provided.', 'success' => false));
}
$extendDir = 'store/[data]/git/sys/extend';
$addonDir = $extendDir . '/addon';
if (!file_exists($extendDir)) {
if (!mkdir($extendDir, 0770, true)) {
logger('Error creating extend folder: ' . $extendDir);
json_return_and_die(array('message' => 'Error creating extend folder: ' . $extendDir, 'success' => false));
}
else {
if (!symlink(realpath('extend/addon'), $addonDir)) {
logger('Error creating symlink to addon folder: ' . $addonDir);
json_return_and_die(array('message' => 'Error creating symlink to addon folder: ' . $addonDir, 'success' => false));
}
}
}
$repoDir = 'store/[data]/git/sys/extend/addon/' . $repoName;
if (!is_dir($repoDir)) {
logger('Repo directory does not exist: ' . $repoDir);
json_return_and_die(array('message' => 'Invalid addon repo.', 'success' => false));
}
if (!is_writable($repoDir)) {
logger('Repo directory not writable to web server: ' . $repoDir);
json_return_and_die(array('message' => 'Repo directory not writable to web server.', 'success' => false));
}
$git = new GitRepo('sys', null, false, $repoName, $repoDir);
try {
if ($git->pull()) {
$files = array_diff(scandir($repoDir), array('.', '..'));
foreach ($files as $file) {
if (is_dir($repoDir . '/' . $file) && $file !== '.git') {
$source = '../extend/addon/' . $repoName . '/' . $file;
$target = realpath('addon/') . '/' . $file;
unlink($target);
if (!symlink($source, $target)) {
logger('Error linking addons to /addon');
json_return_and_die(array('message' => 'Error linking addons to /addon', 'success' => false));
}
}
}
json_return_and_die(array('message' => 'Repo updated.', 'success' => true));
} else {
json_return_and_die(array('message' => 'Error updating addon repo.', 'success' => false));
}
} catch (\PHPGit\Exception\GitException $e) {
json_return_and_die(array('message' => 'Error updating addon repo.', 'success' => false));
}
break;
case 'removerepo':
if (array_key_exists('repoName', $_REQUEST)) {
$repoName = $_REQUEST['repoName'];
} else {
json_return_and_die(array('message' => 'No repo name provided.', 'success' => false));
}
$extendDir = 'store/[data]/git/sys/extend';
$addonDir = $extendDir . '/addon';
if (!file_exists($extendDir)) {
if (!mkdir($extendDir, 0770, true)) {
logger('Error creating extend folder: ' . $extendDir);
json_return_and_die(array('message' => 'Error creating extend folder: ' . $extendDir, 'success' => false));
} else {
if (!symlink(realpath('extend/addon'), $addonDir)) {
logger('Error creating symlink to addon folder: ' . $addonDir);
json_return_and_die(array('message' => 'Error creating symlink to addon folder: ' . $addonDir, 'success' => false));
}
}
}
$repoDir = 'store/[data]/git/sys/extend/addon/' . $repoName;
if (!is_dir($repoDir)) {
logger('Repo directory does not exist: ' . $repoDir);
json_return_and_die(array('message' => 'Invalid addon repo.', 'success' => false));
}
if (!is_writable($repoDir)) {
logger('Repo directory not writable to web server: ' . $repoDir);
json_return_and_die(array('message' => 'Repo directory not writable to web server.', 'success' => false));
}
/// @TODO remove directory and unlink /addon/files
if (rrmdir($repoDir)) {
json_return_and_die(array('message' => 'Repo deleted.', 'success' => true));
} else {
json_return_and_die(array('message' => 'Error deleting addon repo.', 'success' => false));
}
break;
case 'installrepo':
if (array_key_exists('repoURL', $_REQUEST)) {
require_once('library/PHPGit.autoload.php'); // Load PHPGit dependencies
$repoURL = $_REQUEST['repoURL'];
$extendDir = 'store/[data]/git/sys/extend';
$addonDir = $extendDir . '/addon';
if (!file_exists($extendDir)) {
if (!mkdir($extendDir, 0770, true)) {
logger('Error creating extend folder: ' . $extendDir);
json_return_and_die(array('message' => 'Error creating extend folder: ' . $extendDir, 'success' => false));
} else {
if (!symlink(realpath('extend/addon'), $addonDir)) {
logger('Error creating symlink to addon folder: ' . $addonDir);
json_return_and_die(array('message' => 'Error creating symlink to addon folder: ' . $addonDir, 'success' => false));
}
}
}
if (!is_writable($extendDir)) {
logger('Directory not writable to web server: ' . $extendDir);
json_return_and_die(array('message' => 'Directory not writable to web server.', 'success' => false));
}
$repoName = null;
if (array_key_exists('repoName', $_REQUEST) && $_REQUEST['repoName'] !== '') {
$repoName = $_REQUEST['repoName'];
} else {
$repoName = GitRepo::getRepoNameFromURL($repoURL);
}
if (!$repoName) {
logger('Invalid git repo');
json_return_and_die(array('message' => 'Invalid git repo', 'success' => false));
}
$repoDir = $addonDir . '/' . $repoName;
$tempRepoBaseDir = 'store/[data]/git/sys/temp/';
$tempAddonDir = $tempRepoBaseDir . $repoName;
if (!is_writable($addonDir) || !is_writable($tempAddonDir)) {
logger('Temp repo directory or /extend/addon not writable to web server: ' . $tempAddonDir);
json_return_and_die(array('message' => 'Temp repo directory not writable to web server.', 'success' => false));
}
rename($tempAddonDir, $repoDir);
if (!is_writable(realpath('addon/'))) {
logger('/addon directory not writable to web server: ' . $tempAddonDir);
json_return_and_die(array('message' => '/addon directory not writable to web server.', 'success' => false));
}
$files = array_diff(scandir($repoDir), array('.', '..'));
foreach ($files as $file) {
if (is_dir($repoDir . '/' . $file) && $file !== '.git') {
$source = '../extend/addon/' . $repoName . '/' . $file;
$target = realpath('addon/') . '/' . $file;
unlink($target);
if (!symlink($source, $target)) {
logger('Error linking addons to /addon');
json_return_and_die(array('message' => 'Error linking addons to /addon', 'success' => false));
}
}
}
$git = new GitRepo('sys', $repoURL, false, $repoName, $repoDir);
$repo = $git->probeRepo();
json_return_and_die(array('repo' => $repo, 'message' => '', 'success' => true));
}
break;
case 'addrepo':
if (array_key_exists('repoURL', $_REQUEST)) {
require_once('library/PHPGit.autoload.php'); // Load PHPGit dependencies
$repoURL = $_REQUEST['repoURL'];
$extendDir = 'store/[data]/git/sys/extend';
$addonDir = $extendDir . '/addon';
$tempAddonDir = realpath('store/[data]') . '/git/sys/temp';
if (!file_exists($extendDir)) {
if (!mkdir($extendDir, 0770, true)) {
logger('Error creating extend folder: ' . $extendDir);
json_return_and_die(array('message' => 'Error creating extend folder: ' . $extendDir, 'success' => false));
} else {
if (!symlink(realpath('extend/addon'), $addonDir)) {
logger('Error creating symlink to addon folder: ' . $addonDir);
json_return_and_die(array('message' => 'Error creating symlink to addon folder: ' . $addonDir, 'success' => false));
}
}
}
if (!is_dir($tempAddonDir)) {
if (!mkdir($tempAddonDir, 0770, true)) {
logger('Error creating temp plugin repo folder: ' . $tempAddonDir);
json_return_and_die(array('message' => 'Error creating temp plugin repo folder: ' . $tempAddonDir, 'success' => false));
}
}
$repoName = null;
if (array_key_exists('repoName', $_REQUEST) && $_REQUEST['repoName'] !== '') {
$repoName = $_REQUEST['repoName'];
} else {
$repoName = GitRepo::getRepoNameFromURL($repoURL);
}
if (!$repoName) {
logger('Invalid git repo');
json_return_and_die(array('message' => 'Invalid git repo: ' . $repoName, 'success' => false));
}
$repoDir = $tempAddonDir . '/' . $repoName;
if (!is_writable($tempAddonDir)) {
logger('Temporary directory for new addon repo is not writable to web server: ' . $tempAddonDir);
json_return_and_die(array('message' => 'Temporary directory for new addon repo is not writable to web server.', 'success' => false));
}
// clone the repo if new automatically
$git = new GitRepo('sys', $repoURL, true, $repoName, $repoDir);
$remotes = $git->git->remote();
$fetchURL = $remotes['origin']['fetch'];
if ($fetchURL !== $git->url) {
if (rrmdir($repoDir)) {
$git = new GitRepo('sys', $repoURL, true, $repoName, $repoDir);
} else {
json_return_and_die(array('message' => 'Error deleting existing addon repo.', 'success' => false));
}
}
$repo = $git->probeRepo();
$repo['readme'] = $repo['manifest'] = null;
foreach ($git->git->tree('master') as $object) {
if ($object['type'] == 'blob' && (strtolower($object['file']) === 'readme.md' || strtolower($object['file']) === 'readme')) {
$repo['readme'] = MarkdownExtra::defaultTransform($git->git->cat->blob($object['hash']));
} else if ($object['type'] == 'blob' && strtolower($object['file']) === 'manifest.json') {
$repo['manifest'] = $git->git->cat->blob($object['hash']);
}
}
json_return_and_die(array('repo' => $repo, 'message' => '', 'success' => true));
} else {
json_return_and_die(array('message' => 'No repo URL provided', 'success' => false));
}
break;
default:
break;
}
}
}
/**
@@ -408,37 +186,6 @@ class Addons {
usort($plugins,'self::plugin_sort');
$allowManageRepos = false;
if(is_writable('extend/addon') && is_writable('store/[data]')) {
$allowManageRepos = true;
}
$admin_plugins_add_repo_form= replace_macros(
get_markup_template('admin_plugins_addrepo.tpl'), array(
'$post' => 'admin/addons/addrepo',
'$desc' => t('Enter the public git repository URL of the addon repo.'),
'$repoURL' => array('repoURL', t('Addon repo git URL'), '', ''),
'$repoName' => array('repoName', t('Custom repo name'), '', '', t('(optional)')),
'$submit' => t('Download Addon Repo')
)
);
$newRepoModalID = random_string(3);
$newRepoModal = replace_macros(
get_markup_template('generic_modal.tpl'), array(
'$id' => $newRepoModalID,
'$title' => t('Install new repo'),
'$ok' => t('Install'),
'$cancel' => t('Cancel')
)
);
$reponames = $this->listAddonRepos();
$addonrepos = [];
foreach($reponames as $repo) {
$addonrepos[] = array('name' => $repo, 'description' => '');
/// @TODO Parse repo info to provide more information about repos
}
$t = get_markup_template('admin_plugins.tpl');
return replace_macros($t, array(
'$title' => t('Administration'),
@@ -449,37 +196,9 @@ class Addons {
'$plugins' => $plugins,
'$disabled' => t('Disabled - version incompatibility'),
'$form_security_token' => get_form_security_token('admin_addons'),
'$allowManageRepos' => $allowManageRepos,
'$managerepos' => t('Manage Repos'),
'$installedtitle' => t('Installed Addon Repositories'),
'$addnewrepotitle' => t('Install a New Addon Repository'),
'$expandform' => false,
'$form' => $admin_plugins_add_repo_form,
'$newRepoModal' => $newRepoModal,
'$newRepoModalID' => $newRepoModalID,
'$addonrepos' => $addonrepos,
'$repoUpdateButton' => t('Update'),
'$repoBranchButton' => t('Switch branch'),
'$repoRemoveButton' => t('Remove')
));
}
function listAddonRepos() {
$addonrepos = [];
$addonDir = 'extend/addon/';
if(is_dir($addonDir)) {
if ($handle = opendir($addonDir)) {
while (false !== ($entry = readdir($handle))) {
if ($entry != "." && $entry != "..") {
$addonrepos[] = $entry;
}
}
closedir($handle);
}
}
return $addonrepos;
}
static public function plugin_sort($a,$b) {
return(strcmp(strtolower($a[2]['name']),strtolower($b[2]['name'])));
}

View File

@@ -1,159 +0,0 @@
<?php
namespace Zotlabs\Storage;
use PHPGit\Git as PHPGit;
require __DIR__ . '/../../library/PHPGit.autoload.php'; // Load PHPGit dependencies
/**
* Wrapper class for PHPGit class for git repositories managed by Hubzilla
*
* @author Andrew Manning <andrewmanning@grid.reticu.li>
*/
class GitRepo {
public $url = null;
public $name = null;
private $path = null;
private $channel = null;
public $git = null;
private $repoBasePath = null;
function __construct($channel = 'sys', $url = null, $clone = false, $name = null, $path = null) {
if ($channel === 'sys' && !is_site_admin()) {
logger('Only admin can use channel sys');
return null;
}
$this->repoBasePath = __DIR__ . '/../../store/git';
$this->channel = $channel;
$this->git = new PHPGit();
// Allow custom path for repo in the case of , for example
if ($path) {
$this->path = $path;
} else {
$this->path = $this->repoBasePath . "/" . $this->channel . "/" . $this->name;
}
if ($this->isValidGitRepoURL($url)) {
$this->url = $url;
}
if ($name) {
$this->name = $name;
} else {
$this->name = $this->getRepoNameFromURL($url);
}
if (!$this->name) {
logger('Error creating GitRepo. No repo name found.');
return null;
}
if (is_dir($this->path)) {
// ignore the $url input if it exists
// TODO: Check if the path is either empty or is a valid git repo and error if not
$this->git->setRepository($this->path);
// TODO: get repo metadata
return;
}
if ($this->url) {
// create the folder and clone the repo at url to that folder if $clone is true
if ($clone) {
if (mkdir($this->path, 0770, true)) {
$this->git->setRepository($this->path);
if (!$this->cloneRepo()) {
// TODO: throw error
logger('git clone failed: ' . json_encode($this->git));
}
} else {
logger('git repo path could not be created: ' . json_encode($this->git));
}
}
}
}
public function initRepo() {
if(!$this->path) return false;
try {
return $this->git->init($this->path);
} catch (\PHPGit\Exception\GitException $ex) {
return false;
}
}
public function pull() {
try {
$success = $this->git->pull();
} catch (\PHPGit\Exception\GitException $ex) {
return false;
}
return $success;
}
public function getRepoPath() {
return $this->path;
}
public function setRepoPath($directory) {
if (is_dir($directory)) {
$this->path->$directory;
$this->git->setRepository($directory);
return true;
}
return false;
}
public function setIdentity($user_name, $user_email) {
// setup user for commit messages
$this->git->config->set("user.name", $user_name, ['global' => false, 'system' => false]);
$this->git->config->set("user.email", $user_email, ['global' => false, 'system' => false]);
}
public function cloneRepo() {
if (validate_url($this->url) && $this->isValidGitRepoURL($this->url) && is_dir($this->path)) {
return $this->git->clone($this->url, $this->path);
}
}
public function probeRepo() {
$git = $this->git;
$repo = array();
$repo['remote'] = $git->remote();
$repo['branches'] = $git->branch(['all' => true]);
$repo['logs'] = $git->log(array('limit' => 50));
return $repo;
}
// Commit changes to the repo. Default is to stage all changes and commit everything.
public function commit($msg, $options = array()) {
try {
return $this->git->commit($msg, $options);
} catch (\PHPGit\Exception\GitException $ex) {
return false;
}
}
public static function isValidGitRepoURL($url) {
if (validate_url($url) && strrpos(parse_url($url, PHP_URL_PATH), '.')) {
return true;
} else {
return false;
}
}
public static function getRepoNameFromURL($url) {
$urlpath = parse_url($url, PHP_URL_PATH);
$lastslash = strrpos($urlpath, '/') + 1;
$gitext = strrpos($urlpath, '.');
if ($gitext) {
return substr($urlpath, $lastslash, $gitext - $lastslash);
} else {
return null;
}
}
}

View File

@@ -1,45 +1,8 @@
<div class="generic-content-wrapper">
<div class="section-title-wrapper">
{{if $allowManageRepos}}
<div class="float-end">
<button class="btn btn-success btn-sm" onclick="openClose('form');">{{$managerepos}}</button>
</div>
{{/if}}
<h2 id="title">{{$title}} - {{$page}}</h2>
<div class="clear"></div>
</div>
<div id="form" class="section-content-tools-wrapper"{{if !$expandform}} style="display:none;"{{/if}}>
<div class="clear"></div>
<div class="section-title-wrapper" style="margin-top: 20px;">
<h2>{{$installedtitle}}</h2>
<div class="clear"></div>
</div>
<div class="table-responsive section-content-tools-wrapper">
<table class="table table-responsive table-striped table-hover">
{{foreach $addonrepos as $repo}}
<tr>
<td style="width: 70%;">
<span class="float-start">{{$repo.name}}</span><span id="update-message-{{$repo.name}}" style="margin-left: 20px;"></span>
</td>
<td style="width: 15%;">
<button class="btn btn-sm btn-primary float-end" style="margin-left: 10px; margin-right: 10px;" onclick="updateAddonRepo('{{$repo.name}}'); return false;"><i class='bi fa-download'></i>&nbsp;{{$repoUpdateButton}}</button>
</td>
<td style="width: 15%;">
<button class="btn btn-sm btn-danger float-end" style="margin-left: 10px; margin-right: 0px;" onclick="removeAddonRepo('{{$repo.name}}'); return false;"><i class='bi bi-trash'></i>&nbsp;{{$repoRemoveButton}}</button>
</td>
<div class="clear"></div>
</td></tr>
{{/foreach}}
</table>
</div>
<div class="clear"></div>
<div class="section-title-wrapper">
<h2>{{$addnewrepotitle}}</h2>
<div class="clear"></div>
</div>
{{$form}}
</div>
<div class="clear"></div>
<div id="chat-rotator" class="spinner-wrapper">
<div class="spinner s"></div>
@@ -47,9 +10,9 @@
<div class="clear"></div>
<div class="section-content-wrapper-np">
{{foreach $plugins as $p}}
<div class="section-content-tools-wrapper" id="pluginslist">
<div class="section-content-tools-wrapper" id="pluginslist">
<div class="contact-info plugin {{$p.1}}">
{{if ! $p.2.disabled}}
{{if ! $p.2.disabled}}
<a class='toggleplugin' href='{{$baseurl}}/admin/{{$function}}/{{$p.0}}?a=t&amp;t={{$form_security_token}}' title="{{if $p.1==on}}Disable{{else}}Enable{{/if}}" ><i class='bi {{if $p.1==on}}bi-check-square{{else}}bi-square{{/if}} admin-icons'></i></a>
{{else}}
<i class='bi fa-stop admin-icons'></i>
@@ -61,130 +24,6 @@
</div>
</div>
{{/foreach}}
</div>
</div>
{{$newRepoModal}}
<script>
// TODO: Implement a simple interface controller that reconfigures the modal dialog
// for each action in a more organized way
function adminPluginsAddRepo() {
$("#generic-modal-ok-{{$newRepoModalID}}").removeClass('btn-success');
$("#generic-modal-ok-{{$newRepoModalID}}").addClass('btn-primary');
var repoURL = $('#id_repoURL').val();
var repoName = $('#id_repoName').val();
$('#chat-rotator').show();
$.post(
"/admin/addons/addrepo", {repoURL: repoURL, repoName: repoName},
function(response) {
$('#chat-rotator').hide();
if (response.success) {
var modalBody = $('#generic-modal-body-{{$newRepoModalID}}');
modalBody.html('<div>'+response.repo.readme+'</div>');
modalBody.append('<h2>Repo Info</h2><p>Message: ' + response.message + '</p>');
modalBody.append('<h4>Branches</h4><p>'+JSON.stringify(response.repo.branches)+'</p>');
modalBody.append('<h4>Remotes</h4><p>'+JSON.stringify(response.repo.remote)+'</p>');
$('.modal-dialog').width('80%');
$("#generic-modal-ok-{{$newRepoModalID}}").off('click');
$("#generic-modal-ok-{{$newRepoModalID}}").click(function () {
installAddonRepo();
});
$('#generic-modal-{{$newRepoModalID}}').modal();
} else {
window.console.log('Error adding repo :' + response['message']);
}
return false;
},
'json');
}
function installAddonRepo() {
var repoURL = $('#id_repoURL').val();
var repoName = $('#id_repoName').val();
$.post(
"/admin/addons/installrepo", {repoURL: repoURL, repoName: repoName},
function(response) {
if (response.success) {
$('#generic-modal-title-{{$newRepoModalID}}').html('Addon repo installed');
var modalBody = $('#generic-modal-body-{{$newRepoModalID}}');
modalBody.html('<h2>Repo Info</h2><p>Message: ' + response.message + '</p>');
modalBody.append('<h4>Branches</h4><p>'+JSON.stringify(response.repo.branches)+'</p>');
modalBody.append('<h4>Remotes</h4><p>'+JSON.stringify(response.repo.remote)+'</p>');
$('.modal-dialog').width('80%');
//$("#generic-modal-cancel-{{$newRepoModalID}}").hide();
$("#generic-modal-ok-{{$newRepoModalID}}").html('OK');
$("#generic-modal-ok-{{$newRepoModalID}}").removeClass('btn-primary');
$("#generic-modal-ok-{{$newRepoModalID}}").addClass('btn-success');
$("#generic-modal-ok-{{$newRepoModalID}}").off('click');
$("#generic-modal-ok-{{$newRepoModalID}}").click(function () {
$('#generic-modal-{{$newRepoModalID}}').modal('hide');
if(confirm('Repo installed. Click OK to refresh page.')) {
location.reload();
}
});
$('#generic-modal-{{$newRepoModalID}}').modal();
} else {
window.console.log('Error installing repo :' + response['message']);
alert('Error installing addon repo!');
}
return false;
},
'json');
}
function updateAddonRepo(repoName) {
if(confirm('Are you sure you want to update the addon repo ' + repoName + '?')) {
$.post(
"/admin/addons/updaterepo", {repoName: repoName},
function(response) {
if (response.success) {
window.console.log('Addon repo '+repoName+' successfully updated :' + response['message']);
//alert('Addon repo updated.');
$('#update-message-' + repoName).css('background-color', 'yellow');
$('#update-message-' + repoName).html('Repo updated!');
setTimeout(function () {
$('#update-message-' + repoName).html('');
}, 60000);
} else {
window.console.log('Error updating repo :' + response['message']);
//alert('Error updating addon repo!');
$('#update-message-' + repoName).css('background-color', 'red');
$('#update-message-' + repoName).html('Error updating repo!');
setTimeout(function () {
$('#update-message-' + repoName).html('');
}, 60000);
}
return false;
},
'json');
}
}
function switchAddonRepoBranch(repoName) {
window.console.log('switchAddonRepoBranch: ' + repoName);
// TODO: Discover the available branches and create an interface to switch between them
}
function removeAddonRepo(repoName) {
// TODO: Unlink the addons
if(confirm('Are you sure you want to remove the addon repo ' + repoName + '?')) {
$.post(
"/admin/addons/removerepo", {repoName: repoName},
function(response) {
if (response.success) {
window.console.log('Addon repo '+repoName+' successfully removed :' + response['message']);
if(confirm('Repo deleted. Click OK to refresh page.')) {
location.reload();
}
} else {
window.console.log('Error removing repo :' + response['message']);
alert('Error removing addon repo!');
}
return false;
},
'json');
}
}
</script>

View File

@@ -1,9 +0,0 @@
<form id="add-plugin-repo-form" action="{{$post}}" method="post" >
<p class="descriptive-text">{{$desc}}</p>
{{include file="field_input.tpl" field=$repoURL}}
{{include file="field_input.tpl" field=$repoName}}
<div class="btn-group float-end">
<button id="add-plugin-repo-submit" class="btn btn-primary" type="submit" name="submit" onclick="adminPluginsAddRepo(); return false;">{{$submit}}</button>
</div>
</form>