All checks were successful
Build, Push, Publish / Build & Release (push) Successful in 27s
1036 lines
39 KiB
PHP
Executable File
1036 lines
39 KiB
PHP
Executable File
<?php
|
|
/*
|
|
Plugin Name: ICC Upload & Shorten
|
|
Plugin URI: https://github.com/ivancarlosti/YOURLS-Upload-and-Shorten
|
|
Description: Upload a file locally or to AWS S3 and create a short-YOURL for it in one step.
|
|
Version: 1.0
|
|
Author: Ivan Carlos
|
|
Author URI: https://github.com/ivancarlosti
|
|
*/
|
|
|
|
// No direct call
|
|
if (!defined('YOURLS_ABSPATH'))
|
|
die();
|
|
|
|
// Register our plugin admin page
|
|
yourls_add_action('plugins_loaded', 'icc_upload_and_shorten_add_page');
|
|
yourls_add_action('admin_init', 'icc_upload_and_shorten_handle_ajax');
|
|
yourls_add_action('load-plugins_page_icc_upload_and_shorten', 'icc_upload_and_shorten_cleanup_temp'); // Run cleanup on plugin page load
|
|
|
|
function icc_upload_and_shorten_add_page()
|
|
{
|
|
// create entry in the admin's plugin menu
|
|
yourls_register_plugin_page('icc_upload_and_shorten', 'Upload & Shorten', 'icc_upload_and_shorten_do_page');
|
|
}
|
|
|
|
// Handle AJAX requests for chunked uploads
|
|
function icc_upload_and_shorten_handle_ajax()
|
|
{
|
|
if (isset($_POST['action']) && ($_POST['action'] == 'icc_upload_chunk' || $_POST['action'] == 'icc_upload_finish')) {
|
|
// Ensure user is authenticated
|
|
if (!yourls_is_valid_user()) {
|
|
echo json_encode(['status' => 'error', 'message' => 'Authentication failed']);
|
|
die();
|
|
}
|
|
|
|
if ($_POST['action'] == 'icc_upload_chunk') {
|
|
icc_upload_and_shorten_handle_chunk();
|
|
}
|
|
if ($_POST['action'] == 'icc_upload_finish') {
|
|
icc_upload_and_shorten_handle_finish();
|
|
}
|
|
}
|
|
}
|
|
|
|
function icc_upload_and_shorten_handle_chunk()
|
|
{
|
|
$nonce = $_POST['nonce'] ?? '';
|
|
if (!yourls_verify_nonce('icc_upload_chunk', $nonce)) {
|
|
echo json_encode(['status' => 'error', 'message' => 'Security check failed']);
|
|
die();
|
|
}
|
|
|
|
if (!isset($_FILES['file_chunk']) || $_FILES['file_chunk']['error'] != UPLOAD_ERR_OK) {
|
|
echo json_encode(['status' => 'error', 'message' => 'Upload error']);
|
|
die();
|
|
}
|
|
|
|
$upload_id = preg_replace('/[^a-zA-Z0-9_]/', '', $_POST['upload_id']);
|
|
$temp_dir = yourls_get_option('icc_upload_share_dir');
|
|
if (!$temp_dir)
|
|
$temp_dir = sys_get_temp_dir();
|
|
|
|
// Create a temp directory for this upload
|
|
$target_dir = rtrim($temp_dir, '/') . '/icc_temp_' . $upload_id;
|
|
if (!is_dir($target_dir))
|
|
mkdir($target_dir, 0755, true);
|
|
|
|
$chunk_index = intval($_POST['chunk_index']);
|
|
$target_file = $target_dir . '/part_' . $chunk_index;
|
|
|
|
if (move_uploaded_file($_FILES['file_chunk']['tmp_name'], $target_file)) {
|
|
echo json_encode(['status' => 'success']);
|
|
} else {
|
|
echo json_encode(['status' => 'error', 'message' => 'Failed to move chunk']);
|
|
}
|
|
die();
|
|
}
|
|
|
|
function icc_upload_and_shorten_handle_finish()
|
|
{
|
|
$nonce = $_POST['nonce'] ?? '';
|
|
if (!yourls_verify_nonce('icc_upload_chunk', $nonce)) {
|
|
echo json_encode(['status' => 'error', 'message' => 'Security check failed']);
|
|
die();
|
|
}
|
|
|
|
$upload_id = preg_replace('/[^a-zA-Z0-9_]/', '', $_POST['upload_id']);
|
|
$file_name = $_POST['file_name'];
|
|
$temp_dir = yourls_get_option('icc_upload_share_dir');
|
|
if (!$temp_dir)
|
|
$temp_dir = sys_get_temp_dir();
|
|
|
|
$target_dir = rtrim($temp_dir, '/') . '/icc_temp_' . $upload_id;
|
|
$final_file_path = $target_dir . '/' . $file_name;
|
|
|
|
// Assemble chunks
|
|
if ($fp = fopen($final_file_path, 'wb')) {
|
|
$chunks = glob($target_dir . '/part_*');
|
|
natsort($chunks);
|
|
foreach ($chunks as $chunk) {
|
|
$chunk_content = file_get_contents($chunk);
|
|
fwrite($fp, $chunk_content);
|
|
unlink($chunk);
|
|
}
|
|
fclose($fp);
|
|
rmdir($target_dir); // Remove temp dir
|
|
|
|
// now process the file
|
|
// Pass essential POST data for filename conversion if needed
|
|
|
|
$result = icc_upload_and_shorten_process_upload($final_file_path, $file_name);
|
|
|
|
// Since the result is HTML string, we might want to return it or parse it
|
|
// But for this AJAX response we return it in message
|
|
echo json_encode(['status' => 'success', 'message' => $result]);
|
|
} else {
|
|
echo json_encode(['status' => 'error', 'message' => 'Failed to assemble file']);
|
|
}
|
|
die();
|
|
}
|
|
|
|
// Display admin page
|
|
function icc_upload_and_shorten_do_page()
|
|
{
|
|
// Check if a form was submitted
|
|
if (isset($_POST['action']) && $_POST['action'] == 'icc_upload_and_shorten_save') {
|
|
icc_upload_and_shorten_update_settings();
|
|
}
|
|
|
|
// Handle Deletion
|
|
if (isset($_POST['action']) && $_POST['action'] == 'delete_local_file' && isset($_POST['file_name'])) {
|
|
$nonce = $_POST['nonce'] ?? '';
|
|
if (yourls_verify_nonce('icc_delete_local_file', $nonce)) {
|
|
$share_dir = yourls_get_option('icc_upload_share_dir');
|
|
$file_name = $_POST['file_name'];
|
|
// Validating filename to prevent directory traversal
|
|
if (basename($file_name) == $file_name) {
|
|
$file_path = rtrim($share_dir, '/') . '/' . $file_name;
|
|
if (file_exists($file_path)) {
|
|
if (unlink($file_path)) {
|
|
echo "<div class='updated'>File deleted successfully: " . htmlspecialchars($file_name) . "</div>";
|
|
} else {
|
|
echo "<div class='error'>Failed to delete file. Check permissions.</div>";
|
|
}
|
|
} else {
|
|
echo "<div class='error'>File not found.</div>";
|
|
}
|
|
} else {
|
|
echo "<div class='error'>Invalid filename.</div>";
|
|
}
|
|
} else {
|
|
echo "<div class='error'>Security check failed.</div>";
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['action']) && $_POST['action'] == 'delete_file' && isset($_POST['file_key'])) {
|
|
$nonce = $_POST['nonce'] ?? '';
|
|
if (yourls_verify_nonce('icc_delete_file', $nonce)) {
|
|
try {
|
|
$s3_key = yourls_get_option('icc_upload_s3_key');
|
|
$s3_secret = yourls_get_option('icc_upload_s3_secret');
|
|
$s3_region = yourls_get_option('icc_upload_s3_region');
|
|
$s3_bucket = yourls_get_option('icc_upload_s3_bucket');
|
|
|
|
$s3 = icc_get_aws_client($s3_key, $s3_secret, $s3_region);
|
|
if ($s3) {
|
|
$s3->deleteObject([
|
|
'Bucket' => $s3_bucket,
|
|
'Key' => $_POST['file_key']
|
|
]);
|
|
echo "<div class='updated'>File deleted successfully: " . htmlspecialchars($_POST['file_key']) . "</div>";
|
|
} else {
|
|
echo "<div class='error'>Failed to initialize S3 client for deletion.</div>";
|
|
}
|
|
} catch (Aws\S3\Exception\S3Exception $e) {
|
|
echo "<div class='error'>Failed to delete file: " . $e->getMessage() . "</div>";
|
|
}
|
|
} else {
|
|
echo "<div class='error'>Security check failed (Invalid Nonce).</div>";
|
|
}
|
|
}
|
|
|
|
// Manual Cleanup Handler
|
|
if (isset($_POST['action']) && $_POST['action'] == 'icc_manual_cleanup') {
|
|
$nonce = $_POST['nonce'] ?? '';
|
|
if (yourls_verify_nonce('icc_manual_cleanup', $nonce)) {
|
|
echo '<div style="background:#fff; border:1px solid #ccc; padding:10px; margin: 10px 0; max-height: 300px; overflow:auto;">';
|
|
echo '<strong>Starting Manual Cleanup Diagnostics...</strong><br/>';
|
|
|
|
$temp_dir = yourls_get_option('icc_upload_share_dir');
|
|
if (!$temp_dir) $temp_dir = sys_get_temp_dir();
|
|
|
|
echo "Target Directory: " . htmlspecialchars($temp_dir) . "<br/>";
|
|
|
|
if (!is_dir($temp_dir)) {
|
|
echo "<span style='color:red'>Directory does not exist!</span><br/>";
|
|
} elseif (!is_writable($temp_dir)) {
|
|
echo "<span style='color:red'>Directory is not writable! Permissions issues likely.</span><br/>";
|
|
} else {
|
|
$files = scandir($temp_dir);
|
|
$found = 0;
|
|
foreach ($files as $file) {
|
|
if ($file == '.' || $file == '..') continue;
|
|
|
|
// Only look for icc_temp_ folders
|
|
if (strpos($file, 'icc_temp_') !== 0) continue;
|
|
|
|
$path = rtrim($temp_dir, '/') . '/' . $file;
|
|
if (!is_dir($path)) continue;
|
|
|
|
$found++;
|
|
$age = time() - filemtime($path);
|
|
echo "<hr/><strong>Found:</strong> " . htmlspecialchars($file) . "<br/>";
|
|
echo "Path: " . htmlspecialchars($path) . "<br/>";
|
|
echo "Age: " . $age . " seconds (" . round($age/3600, 2) . " hours)<br/>";
|
|
|
|
// Force delete if requested via manual action, or just standard check
|
|
// For manual diagnostics, we'll try to delete anything > 24 hours just like the automated one
|
|
if ($age > 86400) {
|
|
echo "Status: <span style='color:blue'>Older than 24 hours. Attempting deletion...</span><br/>";
|
|
icc_rrmdir($path);
|
|
if (!file_exists($path)) {
|
|
echo "Result: <span style='color:green'>DELETED SUCCESS.</span><br/>";
|
|
} else {
|
|
echo "Result: <span style='color:red'>DELETED FAILED. Check server log/permissions.</span><br/>";
|
|
}
|
|
} else {
|
|
echo "Status: <span style='color:orange'>Kept (Not old enough).</span><br/>";
|
|
}
|
|
}
|
|
|
|
if ($found == 0) {
|
|
echo "No temporary 'icc_temp_' folders found.<br/>";
|
|
}
|
|
}
|
|
echo '<strong>Diagnostics Complete.</strong></div>';
|
|
} else {
|
|
echo "<div class='error'>Security check failed.</div>";
|
|
}
|
|
}
|
|
|
|
$message = '';
|
|
if (isset($_POST['submit']) && $_POST['submit'] == 'Upload')
|
|
$message = icc_upload_and_shorten_process_upload();
|
|
|
|
$storage_type = yourls_get_option('icc_upload_storage_type', 'local');
|
|
$share_url = yourls_get_option('icc_upload_share_url');
|
|
$share_dir = yourls_get_option('icc_upload_share_dir');
|
|
$suffix_length = yourls_get_option('icc_upload_suffix_length', 4);
|
|
|
|
// S3 Config
|
|
$s3_key = yourls_get_option('icc_upload_s3_key');
|
|
$s3_secret = yourls_get_option('icc_upload_s3_secret');
|
|
$s3_region = yourls_get_option('icc_upload_s3_region');
|
|
$s3_bucket = yourls_get_option('icc_upload_s3_bucket');
|
|
$s3_disable_acl = yourls_get_option('icc_upload_s3_disable_acl', false);
|
|
|
|
// input form
|
|
echo '
|
|
<h2>Upload & Shorten</h2>
|
|
<h3>Send a file to ' . ($storage_type == 's3' ? 'AWS S3' : 'your webserver') . ' and create a short-URL for it.</h3>';
|
|
|
|
// Limits Diagnostics
|
|
$max_upload = ini_get('upload_max_filesize');
|
|
$max_post = ini_get('post_max_size');
|
|
echo "<p><small>Server Limits: Upload Max Filesize: <strong>$max_upload</strong>, Post Max Size: <strong>$max_post</strong>. <br>The <strong>Smart Uploader</strong> bypasses these limits by splitting files into chunks!</small></p>";
|
|
|
|
if (!empty($message)) {
|
|
echo "<p><strong>$message</strong></p>";
|
|
}
|
|
|
|
if (
|
|
($storage_type == 'local' && (empty($share_url) || empty($share_dir))) ||
|
|
($storage_type == 's3' && (empty($s3_key) || empty($s3_secret) || empty($s3_region) || empty($s3_bucket)))
|
|
) {
|
|
echo '<p style="color:red"><strong>Please configure the plugin below before using this plugin.</strong></p>';
|
|
}
|
|
|
|
$chunk_nonce = yourls_create_nonce('icc_upload_chunk');
|
|
|
|
echo '
|
|
<form id="icc_upload_form" method="post" enctype="multipart/form-data">
|
|
<input type="hidden" name="action" value="upload_file" />
|
|
<input type="hidden" id="chunk_nonce" value="' . $chunk_nonce . '" />
|
|
|
|
<fieldset> <legend>Select a file </legend>
|
|
<p><input type="file" id="file_upload" name="file_upload" /></p>
|
|
|
|
<div id="progress_container" style="display:none; width: 100%; background-color: #f3f3f3; border: 1px solid #ccc; margin-top: 10px;">
|
|
<div id="progress_bar" style="width: 0%; height: 20px; background-color: #4caf50; text-align: center; color: white;">0%</div>
|
|
</div>
|
|
<p id="upload_status"></p>
|
|
|
|
</fieldset>';
|
|
|
|
// YOURLS options
|
|
echo '
|
|
<fieldset> <legend>YOURLS database options</legend>
|
|
|
|
<p><label for="custom_shortname">Custom shortname: </label>
|
|
<input type="text" id="custom_shortname" name="custom_shortname" />
|
|
|
|
<label for="custom_title">Custom title: </label>
|
|
<input type="text" id="custom_title" name="custom_title" /></p>
|
|
</fieldset>';
|
|
|
|
// filename handling
|
|
echo '
|
|
<fieldset> <legend>Filename conversions (optional)</legend>
|
|
|
|
<p><input type="radio" id="safe_filename" name="convert_filename" value="browser-safe" checked="checked" />
|
|
<label for="safe_filename">Browser-safe filename </label>
|
|
<small>(Recommended if the file should be accessed by web-browsers.)<br/ >
|
|
Ex.: "my not safe&clean filename #1.txt" -> https://example.com/my_not_safe_clean_filename_1.txt </small></p>
|
|
|
|
<p><input type="radio" id="safe_suffix" name="convert_filename" value="safe_suffix" />
|
|
<label for="safe_suffix">Browser-safe filename + random suffix </label>
|
|
<small>(Adds a random alphanumeric suffix to the filename.)<br/ >
|
|
Ex.: "file.txt" -> https://example.com/file_a1b2.txt </small></p>
|
|
|
|
<p><input type="radio" id="random_filename" name="convert_filename" value="randomized" />
|
|
<label for="random_filename">Randomize filename </label>
|
|
<small>(Browser-safe filenames with a slight protection against systematic crawling your web-directory.)<br/ >
|
|
Ex.: "mypicture.jpg" -> https://example.com/9a3e97434689.jpg </small></p>
|
|
|
|
</fieldset>';
|
|
|
|
// do it!
|
|
echo '
|
|
<p><input type="submit" id="submit_btn" name="submit" value="Upload" /></p>
|
|
</form>';
|
|
|
|
// JS for Chunked Upload
|
|
echo '
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
var form = document.getElementById("icc_upload_form");
|
|
var fileInput = document.getElementById("file_upload");
|
|
var progressBar = document.getElementById("progress_bar");
|
|
var progressContainer = document.getElementById("progress_container");
|
|
var status = document.getElementById("upload_status");
|
|
var submitBtn = document.getElementById("submit_btn");
|
|
|
|
form.onsubmit = function(event) {
|
|
if (fileInput.files.length === 0) return;
|
|
|
|
// Only capture if file is selected
|
|
event.preventDefault();
|
|
|
|
var file = fileInput.files[0];
|
|
var chunkSize = 2 * 1024 * 1024; // 2MB
|
|
var totalChunks = Math.ceil(file.size / chunkSize);
|
|
var chunkIndex = 0;
|
|
var uploadId = Date.now() + "_" + Math.random().toString(36).substr(2, 9);
|
|
var nonce = document.getElementById("chunk_nonce").value;
|
|
|
|
progressContainer.style.display = "block";
|
|
submitBtn.disabled = true;
|
|
status.innerHTML = "Uploading chunk 1 of " + totalChunks + "...";
|
|
|
|
function uploadNextChunk() {
|
|
var start = chunkIndex * chunkSize;
|
|
var end = Math.min(start + chunkSize, file.size);
|
|
var chunk = file.slice(start, end);
|
|
|
|
var formData = new FormData();
|
|
formData.append("action", "icc_upload_chunk");
|
|
formData.append("file_chunk", chunk);
|
|
formData.append("chunk_index", chunkIndex);
|
|
formData.append("upload_id", uploadId);
|
|
formData.append("nonce", nonce);
|
|
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open("POST", window.location.href, true);
|
|
|
|
xhr.onload = function() {
|
|
if (xhr.status === 200) {
|
|
try {
|
|
var resp = JSON.parse(xhr.responseText);
|
|
if (resp.status === "success") {
|
|
chunkIndex++;
|
|
var percent = Math.round((chunkIndex / totalChunks) * 100);
|
|
progressBar.style.width = percent + "%";
|
|
progressBar.innerText = percent + "%";
|
|
|
|
if (chunkIndex < totalChunks) {
|
|
status.innerHTML = "Uploading chunk " + (chunkIndex + 1) + " of " + totalChunks + "...";
|
|
uploadNextChunk();
|
|
} else {
|
|
status.innerHTML = "Refining upload...";
|
|
finishUpload();
|
|
}
|
|
} else {
|
|
status.innerHTML = "Error: " + resp.message;
|
|
submitBtn.disabled = false;
|
|
}
|
|
} catch(e) {
|
|
status.innerHTML = "Error parsing server response.";
|
|
submitBtn.disabled = false;
|
|
}
|
|
} else {
|
|
status.innerHTML = "Server error " + xhr.status;
|
|
submitBtn.disabled = false;
|
|
}
|
|
};
|
|
|
|
xhr.send(formData);
|
|
}
|
|
|
|
function finishUpload() {
|
|
var formData = new FormData();
|
|
// Append all form fields except the file input
|
|
var elements = form.elements;
|
|
for (var i = 0; i < elements.length; i++) {
|
|
var el = elements[i];
|
|
if (el.name && el.type !== \'file\' && el.name !== \'submit\') { // Skip file and submit button
|
|
if (el.type === \'radio\' || el.type === \'checkbox\') {
|
|
if (el.checked) formData.append(el.name, el.value);
|
|
} else {
|
|
formData.append(el.name, el.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
formData.append("action", "icc_upload_finish");
|
|
formData.append("upload_id", uploadId);
|
|
formData.append("file_name", file.name);
|
|
formData.append("nonce", nonce);
|
|
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open("POST", window.location.href, true);
|
|
xhr.onload = function() {
|
|
submitBtn.disabled = false;
|
|
if (xhr.status === 200) {
|
|
try {
|
|
var resp = JSON.parse(xhr.responseText);
|
|
if (resp.status === "success") {
|
|
status.innerHTML = "Upload Complete!";
|
|
// Instead of redirecting, we replace body or show message
|
|
// Ideally, we reload or show the result HTML
|
|
var resultDiv = document.createElement("div");
|
|
resultDiv.innerHTML = resp.message;
|
|
form.parentNode.insertBefore(resultDiv, form);
|
|
form.reset();
|
|
progressContainer.style.display = "none";
|
|
} else {
|
|
status.innerHTML = "Error finishing upload: " + resp.message;
|
|
}
|
|
} catch(e) {
|
|
status.innerHTML = "Error finishing upload (Invalid JSON).";
|
|
console.log(xhr.responseText);
|
|
}
|
|
}
|
|
};
|
|
xhr.send(formData);
|
|
}
|
|
|
|
uploadNextChunk();
|
|
};
|
|
});
|
|
</script>
|
|
';
|
|
|
|
// File Manager
|
|
if ($storage_type == 's3' && !empty($s3_key) && !empty($s3_secret) && !empty($s3_bucket)) {
|
|
icc_upload_and_shorten_file_manager($s3_key, $s3_secret, $s3_region, $s3_bucket);
|
|
} elseif ($storage_type == 'local' && !empty($share_dir)) {
|
|
icc_upload_and_shorten_local_file_manager($share_dir, $share_url);
|
|
}
|
|
|
|
// Configuration Section
|
|
$nonce = yourls_create_nonce('icc_upload_and_shorten_settings');
|
|
echo '
|
|
<hr />
|
|
<h3>Configuration</h3>
|
|
<form method="post">
|
|
<input type="hidden" name="action" value="icc_upload_and_shorten_save" />
|
|
<input type="hidden" name="nonce" value="' . $nonce . '" />
|
|
|
|
<p>
|
|
<label for="icc_upload_storage_type"><strong>Storage Type:</strong></label><br />
|
|
<select name="icc_upload_storage_type" id="icc_upload_storage_type">
|
|
<option value="local" ' . ($storage_type == 'local' ? 'selected' : '') . '>Local Server</option>
|
|
<option value="s3" ' . ($storage_type == 's3' ? 'selected' : '') . '>AWS S3</option>
|
|
</select>
|
|
</p>
|
|
|
|
<h4>Local Server Settings</h4>
|
|
<p>
|
|
<label for="icc_upload_share_url">Share URL (The web URL path where YOURLS short-links will redirect to):</label><br />
|
|
<small>Example: <code>https://example.com/file/</code></small><br />
|
|
<input type="text" id="icc_upload_share_url" name="icc_upload_share_url" value="' . $share_url . '" size="50" />
|
|
</p>
|
|
<p>
|
|
<label for="icc_upload_share_dir">Share Directory (The physical path where uploads are stored):</label><br />
|
|
<small>Example: <code>/home/username/htdocs/example.com/file/</code> (Directory must exist)</small><br />
|
|
<input type="text" id="icc_upload_share_dir" name="icc_upload_share_dir" value="' . $share_dir . '" size="50" />
|
|
</p>
|
|
|
|
<h4>AWS S3 Settings</h4>
|
|
<p>
|
|
<label for="icc_upload_s3_key">AWS Access Key:</label><br />
|
|
<input type="text" id="icc_upload_s3_key" name="icc_upload_s3_key" value="' . $s3_key . '" size="50" />
|
|
</p>
|
|
<p>
|
|
<label for="icc_upload_s3_secret">AWS Secret Key:</label><br />
|
|
<input type="password" id="icc_upload_s3_secret" name="icc_upload_s3_secret" value="' . $s3_secret . '" size="50" />
|
|
</p>
|
|
<p>
|
|
<label for="icc_upload_s3_region">AWS Region:</label><br />
|
|
<input type="text" id="icc_upload_s3_region" name="icc_upload_s3_region" value="' . $s3_region . '" size="20" placeholder="us-east-1" />
|
|
</p>
|
|
<p>
|
|
<label for="icc_upload_s3_bucket">S3 Bucket Name:</label><br />
|
|
<input type="text" id="icc_upload_s3_bucket" name="icc_upload_s3_bucket" value="' . $s3_bucket . '" size="50" />
|
|
</p>
|
|
<p>
|
|
<input type="checkbox" id="icc_upload_s3_disable_acl" name="icc_upload_s3_disable_acl" ' . ($s3_disable_acl ? 'checked' : '') . ' />
|
|
<label for="icc_upload_s3_disable_acl"><strong>Disable ACLs</strong> (Check this if your bucket has "Block public access" or "Bucket Owner Enforced" enabled)</label>
|
|
</p>
|
|
|
|
<h4>General Settings</h4>
|
|
<p>
|
|
<label for="icc_upload_suffix_length">Random Suffix Length (For "Browser-safe + random suffix" option):</label><br />
|
|
<input type="number" id="icc_upload_suffix_length" name="icc_upload_suffix_length" value="' . $suffix_length . '" min="1" max="32" />
|
|
</p>
|
|
|
|
<p>
|
|
<label><strong>Diagnostics:</strong></label><br />
|
|
<a href="javascript:void(0);" onclick="document.getElementById(\'icc_cleanup_form\').submit();" class="button">Run Cleanup & Diagnostics Now</a>
|
|
<small> (Checks for \'icc_temp_\' folders older than 24 hours and attempts to delete them)</small>
|
|
</p>
|
|
|
|
<p><input type="submit" value="Save Configuration" class="button-primary" /></p>
|
|
</form>
|
|
|
|
<form id="icc_cleanup_form" method="post" style="display:none;">
|
|
<input type="hidden" name="action" value="icc_manual_cleanup" />
|
|
<input type="hidden" name="nonce" value="' . yourls_create_nonce('icc_manual_cleanup') . '" />
|
|
</form>
|
|
';
|
|
|
|
// footer
|
|
echo '
|
|
|
|
<hr style="margin-top: 40px" />
|
|
<p><strong><a href="https://ivancarlos.me/" target="_blank">Ivan Carlos</a></strong> »
|
|
<a href="https://buymeacoffee.com/ivancarlos" target="_blank">Buy Me a Coffee</a></p>';
|
|
}
|
|
|
|
function icc_upload_and_shorten_update_settings()
|
|
{
|
|
yourls_verify_nonce('icc_upload_and_shorten_settings', $_REQUEST['nonce']);
|
|
|
|
if (isset($_POST['icc_upload_storage_type']))
|
|
yourls_update_option('icc_upload_storage_type', $_POST['icc_upload_storage_type']);
|
|
|
|
if (isset($_POST['icc_upload_share_url']))
|
|
yourls_update_option('icc_upload_share_url', rtrim($_POST['icc_upload_share_url'], '/') . '/');
|
|
|
|
if (isset($_POST['icc_upload_share_dir']))
|
|
yourls_update_option('icc_upload_share_dir', rtrim($_POST['icc_upload_share_dir'], '/') . '/');
|
|
|
|
if (isset($_POST['icc_upload_s3_key']))
|
|
yourls_update_option('icc_upload_s3_key', trim($_POST['icc_upload_s3_key']));
|
|
if (isset($_POST['icc_upload_s3_secret']))
|
|
yourls_update_option('icc_upload_s3_secret', trim($_POST['icc_upload_s3_secret']));
|
|
if (isset($_POST['icc_upload_s3_region']))
|
|
yourls_update_option('icc_upload_s3_region', trim($_POST['icc_upload_s3_region']));
|
|
if (isset($_POST['icc_upload_s3_bucket']))
|
|
yourls_update_option('icc_upload_s3_bucket', trim($_POST['icc_upload_s3_bucket']));
|
|
|
|
if (isset($_POST['icc_upload_s3_disable_acl'])) {
|
|
yourls_update_option('icc_upload_s3_disable_acl', true);
|
|
} else {
|
|
yourls_update_option('icc_upload_s3_disable_acl', false);
|
|
}
|
|
|
|
if (isset($_POST['icc_upload_suffix_length']))
|
|
yourls_update_option('icc_upload_suffix_length', intval($_POST['icc_upload_suffix_length']));
|
|
|
|
echo "<div class='updated'>Settings saved</div>";
|
|
}
|
|
|
|
// Local File Manager Function
|
|
function icc_upload_and_shorten_local_file_manager($dir, $url)
|
|
{
|
|
echo '<hr />';
|
|
echo '<h3>Local File Manager</h3>';
|
|
|
|
if (!is_dir($dir)) {
|
|
echo '<p style="color:red">Directory not found: ' . htmlspecialchars($dir) . '</p>';
|
|
return;
|
|
}
|
|
|
|
$raw_files = scandir($dir);
|
|
$files = [];
|
|
foreach ($raw_files as $f) {
|
|
if ($f == '.' || $f == '..')
|
|
continue;
|
|
$full_path = rtrim($dir, '/') . '/' . $f;
|
|
// Exclude directories (like the temp ones if they exist)
|
|
if (!is_dir($full_path)) {
|
|
$files[] = $f;
|
|
}
|
|
}
|
|
|
|
// Sort by modification time (Newest first)
|
|
usort($files, function ($a, $b) use ($dir) {
|
|
return filemtime(rtrim($dir, '/') . '/' . $b) - filemtime(rtrim($dir, '/') . '/' . $a);
|
|
});
|
|
|
|
// $files = array_values($files); // Already indexed 0..n by sorting
|
|
|
|
// Pagination
|
|
$per_page = 20;
|
|
$total_files = count($files);
|
|
$total_pages = ceil($total_files / $per_page);
|
|
$current_page = isset($_GET['local_page']) ? max(1, intval($_GET['local_page'])) : 1;
|
|
$offset = ($current_page - 1) * $per_page;
|
|
|
|
$page_files = array_slice($files, $offset, $per_page);
|
|
|
|
if (empty($page_files)) {
|
|
echo '<p>No files found.</p>';
|
|
} else {
|
|
$nonce = yourls_create_nonce('icc_delete_local_file');
|
|
echo '<table class="widefat" style="margin-top:10px;">';
|
|
echo '<thead><tr><th>File Name</th><th>Size</th><th>Last Modified</th><th>Action</th></tr></thead>';
|
|
echo '<tbody>';
|
|
|
|
foreach ($page_files as $file) {
|
|
$filepath = rtrim($dir, '/') . '/' . $file;
|
|
$size = file_exists($filepath) ? round(filesize($filepath) / 1024, 2) . ' KB' : 'N/A';
|
|
$date = file_exists($filepath) ? date("Y-m-d H:i:s", filemtime($filepath)) : 'N/A';
|
|
$file_url = rtrim($url, '/') . '/' . $file;
|
|
|
|
echo '<tr>';
|
|
echo '<td><a href="' . htmlspecialchars($file_url) . '" target="_blank">' . htmlspecialchars($file) . '</a></td>';
|
|
echo '<td>' . $size . '</td>';
|
|
echo '<td>' . $date . '</td>';
|
|
echo '<td>';
|
|
echo '<form method="post" style="display:inline;" onsubmit="return confirm(\'Are you sure you want to delete ' . htmlspecialchars($file, ENT_QUOTES) . '?\');">';
|
|
echo '<input type="hidden" name="action" value="delete_local_file" />';
|
|
echo '<input type="hidden" name="file_name" value="' . htmlspecialchars($file) . '" />';
|
|
echo '<input type="hidden" name="nonce" value="' . $nonce . '" />';
|
|
echo '<input type="submit" value="Delete" class="button-secondary" />';
|
|
echo '</form>';
|
|
echo '</td>';
|
|
echo '</tr>';
|
|
}
|
|
echo '</tbody>';
|
|
echo '</table>';
|
|
|
|
// Pagination Controls
|
|
if ($total_pages > 1) {
|
|
echo '<div class="tablenav"><div class="tablenav-pages">';
|
|
echo '<span class="displaying-num">' . $total_files . ' items</span>';
|
|
$url = 'plugins.php?page=icc_upload_and_shorten';
|
|
|
|
if ($current_page > 1) {
|
|
echo '<a class="button" href="' . $url . '&local_page=1">« First</a> ';
|
|
echo '<a class="button" href="' . $url . '&local_page=' . ($current_page - 1) . '">‹ Previous</a> ';
|
|
}
|
|
|
|
echo '<span class="current-page"> Page ' . $current_page . ' of ' . $total_pages . ' </span>';
|
|
|
|
if ($current_page < $total_pages) {
|
|
echo ' <a class="button" href="' . $url . '&local_page=' . ($current_page + 1) . '">Next ›</a> ';
|
|
echo '<a class="button" href="' . $url . '&local_page=' . $total_pages . '">Last »</a>';
|
|
}
|
|
echo '</div></div>';
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Recursive directory removal
|
|
function icc_rrmdir($dir)
|
|
{
|
|
if (is_dir($dir)) {
|
|
$objects = scandir($dir);
|
|
foreach ($objects as $object) {
|
|
if ($object != "." && $object != "..") {
|
|
if (is_dir($dir . "/" . $object) && !is_link($dir . "/" . $object))
|
|
icc_rrmdir($dir . "/" . $object);
|
|
else
|
|
unlink($dir . "/" . $object);
|
|
}
|
|
}
|
|
rmdir($dir);
|
|
}
|
|
}
|
|
|
|
// Cleanup Temp Folders
|
|
function icc_upload_and_shorten_cleanup_temp()
|
|
{
|
|
$temp_dir = yourls_get_option('icc_upload_share_dir');
|
|
if (!$temp_dir)
|
|
$temp_dir = sys_get_temp_dir();
|
|
|
|
if (!is_dir($temp_dir))
|
|
return;
|
|
|
|
// Scan for icc_temp_* directories
|
|
$files = scandir($temp_dir);
|
|
foreach ($files as $file) {
|
|
if ($file == '.' || $file == '..')
|
|
continue;
|
|
|
|
$path = rtrim($temp_dir, '/') . '/' . $file;
|
|
if (is_dir($path) && strpos($file, 'icc_temp_') === 0) {
|
|
// Check age (1 hour = 3600 seconds)
|
|
if (filemtime($path) < (time() - 86400)) {
|
|
icc_rrmdir($path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for AWS SDK
|
|
function icc_get_aws_client($key, $secret, $region)
|
|
{
|
|
if (!file_exists(dirname(__FILE__) . '/aws.phar')) {
|
|
return false;
|
|
}
|
|
require_once dirname(__FILE__) . '/aws.phar';
|
|
|
|
try {
|
|
$s3 = new Aws\S3\S3Client([
|
|
'version' => 'latest',
|
|
'region' => $region,
|
|
'credentials' => [
|
|
'key' => $key,
|
|
'secret' => $secret,
|
|
],
|
|
]);
|
|
return $s3;
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// S3 File Manager Function
|
|
function icc_upload_and_shorten_file_manager($key, $secret, $region, $bucket)
|
|
{
|
|
echo '<hr />';
|
|
echo '<h3>S3 File Manager</h3>';
|
|
|
|
$s3 = icc_get_aws_client($key, $secret, $region);
|
|
if (!$s3) {
|
|
echo '<p style="color:red">Failed to initialize AWS Client.</p>';
|
|
return;
|
|
}
|
|
|
|
// Pagination
|
|
$continuation_token = isset($_GET['s3_next_token']) ? $_GET['s3_next_token'] : null;
|
|
|
|
try {
|
|
$params = [
|
|
'Bucket' => $bucket,
|
|
'MaxKeys' => 20
|
|
];
|
|
|
|
if ($continuation_token) {
|
|
$params['ContinuationToken'] = $continuation_token;
|
|
}
|
|
|
|
$objects = $s3->listObjectsV2($params);
|
|
|
|
if (!isset($objects['Contents']) || empty($objects['Contents'])) {
|
|
echo '<p>No files found in bucket.</p>';
|
|
if ($continuation_token) {
|
|
echo '<p><a href="plugins.php?page=icc_upload_and_shorten" class="button">Start Over</a></p>';
|
|
}
|
|
} else {
|
|
$nonce = yourls_create_nonce('icc_delete_file');
|
|
echo '<table class="widefat" style="margin-top:10px;">';
|
|
echo '<thead><tr><th>File Name</th><th>Size</th><th>Last Modified</th><th>Action</th></tr></thead>';
|
|
echo '<tbody>';
|
|
foreach ($objects['Contents'] as $object) {
|
|
// Construct the file URL (Path-style S3 URL format)
|
|
$file_url = "https://s3.{$region}.amazonaws.com/{$bucket}/" . $object['Key'];
|
|
|
|
echo '<tr>';
|
|
echo '<td><a href="' . htmlspecialchars($file_url) . '" target="_blank">' . htmlspecialchars($object['Key']) . '</a></td>';
|
|
echo '<td>' . round($object['Size'] / 1024, 2) . ' KB</td>';
|
|
echo '<td>' . $object['LastModified'] . '</td>';
|
|
echo '<td>';
|
|
echo '<form method="post" style="display:inline;" onsubmit="return confirm(\'Are you sure you want to delete ' . htmlspecialchars($object['Key'], ENT_QUOTES) . '?\');">';
|
|
echo '<input type="hidden" name="action" value="delete_file" />';
|
|
echo '<input type="hidden" name="file_key" value="' . htmlspecialchars($object['Key']) . '" />';
|
|
echo '<input type="hidden" name="nonce" value="' . $nonce . '" />';
|
|
echo '<input type="submit" value="Delete" class="button-secondary" />';
|
|
echo '</form>';
|
|
echo '</td>';
|
|
echo '</tr>';
|
|
}
|
|
echo '</tbody>';
|
|
echo '</table>';
|
|
echo '<p><small>Showing files from S3 bucket.</small></p>';
|
|
|
|
// Pagination History (to allow 'Previous')
|
|
$history_raw = isset($_GET['s3_history']) ? $_GET['s3_history'] : '';
|
|
$history = $history_raw ? explode(',', $history_raw) : [];
|
|
|
|
// Pagination Controls
|
|
echo '<div class="tablenav"><div class="tablenav-pages">';
|
|
$url_base = 'plugins.php?page=icc_upload_and_shorten';
|
|
|
|
// First Page
|
|
if ($continuation_token) {
|
|
echo '<a class="button" href="' . $url_base . '">« First</a> ';
|
|
}
|
|
|
|
// Previous Page
|
|
if (!empty($history)) {
|
|
$prev_token = array_pop($history);
|
|
$prev_history = implode(',', $history);
|
|
$prev_url = $url_base;
|
|
if ($prev_token && $prev_token !== '__TOP__') {
|
|
$prev_url .= '&s3_next_token=' . urlencode($prev_token);
|
|
}
|
|
if ($prev_history) {
|
|
$prev_url .= '&s3_history=' . urlencode($prev_history);
|
|
}
|
|
echo '<a class="button" href="' . $prev_url . '">‹ Previous</a> ';
|
|
}
|
|
|
|
// Next Page
|
|
if (isset($objects['NextContinuationToken'])) {
|
|
$next_token = $objects['NextContinuationToken'];
|
|
// Append current token to history
|
|
$current_history = $history_raw;
|
|
$token_to_add = $continuation_token ? $continuation_token : '__TOP__';
|
|
if ($current_history) {
|
|
$current_history .= ',' . $token_to_add;
|
|
} else {
|
|
$current_history = $token_to_add;
|
|
}
|
|
echo '<a class="next-page button" href="' . $url_base . '&s3_next_token=' . urlencode($next_token) . '&s3_history=' . urlencode($current_history) . '">Next ›</a>';
|
|
}
|
|
echo '</div></div>';
|
|
}
|
|
} catch (Aws\S3\Exception\S3Exception $e) {
|
|
echo '<p style="color:red">Error listing files: ' . $e->getMessage() . '</p>';
|
|
}
|
|
}
|
|
|
|
// Update option in database
|
|
function icc_upload_and_shorten_process_upload($local_file_path = null, $original_filename = null)
|
|
{
|
|
// If not coming from chunked upload, standard validations
|
|
if (!$local_file_path) {
|
|
// did the user select any file?
|
|
if ($_FILES['file_upload']['error'] == UPLOAD_ERR_NO_FILE) {
|
|
return 'You need to select a file to upload.';
|
|
}
|
|
}
|
|
|
|
// Increase limits for processing large files
|
|
set_time_limit(0);
|
|
|
|
$storage_type = yourls_get_option('icc_upload_storage_type', 'local');
|
|
|
|
// Check Config
|
|
if ($storage_type == 'local') {
|
|
$my_url = yourls_get_option('icc_upload_share_url');
|
|
$my_uploaddir = yourls_get_option('icc_upload_share_dir');
|
|
if (empty($my_url) || empty($my_uploaddir))
|
|
return 'Plugin not configured for local storage.';
|
|
|
|
// Check if directory exists and is writable
|
|
if (!is_dir($my_uploaddir) || !is_writable($my_uploaddir)) {
|
|
return 'Upload directory does not exist or is not writable: ' . $my_uploaddir;
|
|
}
|
|
} elseif ($storage_type == 's3') {
|
|
$key = yourls_get_option('icc_upload_s3_key');
|
|
$secret = yourls_get_option('icc_upload_s3_secret');
|
|
$region = yourls_get_option('icc_upload_s3_region');
|
|
$bucket = yourls_get_option('icc_upload_s3_bucket');
|
|
$disable_acl = yourls_get_option('icc_upload_s3_disable_acl', false);
|
|
if (empty($key) || empty($secret) || empty($region) || empty($bucket))
|
|
return 'Plugin not configured for S3 storage.';
|
|
|
|
$s3 = icc_get_aws_client($key, $secret, $region);
|
|
if (!$s3)
|
|
return 'AWS SDK not found or failed to initialize, please ensure aws.phar is in the plugin folder.';
|
|
}
|
|
|
|
$file_name_to_use = $local_file_path ? $original_filename : $_FILES['file_upload']['name'];
|
|
|
|
// Handle the filename's extension
|
|
$my_upload_extension = pathinfo($file_name_to_use, PATHINFO_EXTENSION);
|
|
|
|
// If there is any extension at all then append it with a leading dot
|
|
$my_extension = '';
|
|
if (isset($my_upload_extension) && $my_upload_extension != NULL) {
|
|
$my_extension = '.' . $my_upload_extension;
|
|
}
|
|
|
|
$my_upload_filename = pathinfo($file_name_to_use, PATHINFO_FILENAME);
|
|
$my_filename = $my_upload_filename; // Default
|
|
|
|
if (isset($_POST['convert_filename'])) {
|
|
switch ($_POST['convert_filename']) {
|
|
case 'browser-safe': {
|
|
// make the filename web-safe:
|
|
$my_filename_trim = trim($my_upload_filename);
|
|
$my_filename_trim = strtolower($my_filename_trim); // Force lowercase
|
|
$my_extension = strtolower($my_extension);
|
|
$my_RemoveChars = array("([^()_\-\.,0-9a-zA-Z\[\]])"); // replace what's NOT in here!
|
|
$my_filename = preg_replace($my_RemoveChars, "_", $my_filename_trim);
|
|
$my_filename = preg_replace("(_{2,})", "_", $my_filename);
|
|
$my_extension = preg_replace($my_RemoveChars, "_", $my_extension);
|
|
$my_extension = preg_replace("(_{2,})", "_", $my_extension);
|
|
}
|
|
break;
|
|
|
|
case 'safe_suffix': {
|
|
// browser-safe + random suffix
|
|
$my_filename_trim = trim($my_upload_filename);
|
|
$my_filename_trim = strtolower($my_filename_trim); // Force lowercase
|
|
$my_extension = strtolower($my_extension);
|
|
$my_RemoveChars = array("([^()_\-\.,0-9a-zA-Z\[\]])");
|
|
$my_filename = preg_replace($my_RemoveChars, "_", $my_filename_trim);
|
|
$my_filename = preg_replace("(_{2,})", "_", $my_filename);
|
|
$my_extension = preg_replace($my_RemoveChars, "_", $my_extension);
|
|
$my_extension = preg_replace("(_{2,})", "_", $my_extension);
|
|
|
|
$suffix_length = yourls_get_option('icc_upload_suffix_length', 4);
|
|
$suffix = substr(str_shuffle('abcdefghijklmnopqrstuvwxyz0123456789'), 0, $suffix_length);
|
|
$my_filename .= '_' . $suffix;
|
|
}
|
|
break;
|
|
|
|
case 'randomized': {
|
|
// make up a random name for the uploaded file
|
|
$my_filename = substr(md5($my_upload_filename . strtotime("now")), 0, 12);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// avoid duplicate filenames
|
|
if ($storage_type == 'local') {
|
|
$my_count = 2;
|
|
$my_path = $my_uploaddir . $my_filename . $my_extension;
|
|
$my_final_file_name = $my_filename . $my_extension;
|
|
|
|
while (file_exists($my_path)) {
|
|
$my_path = $my_uploaddir . $my_filename . '.' . $my_count . $my_extension;
|
|
$my_final_file_name = $my_filename . '.' . $my_count . $my_extension;
|
|
$my_count++;
|
|
}
|
|
} else {
|
|
// For S3, exact duplicate check is hard without API call, so we assume timestamp or suffix makes it unique enough
|
|
// Or we can just overwrite as S3 versioning might be on, but user asked for simple upload
|
|
// We will just use the name derived.
|
|
$my_final_file_name = $my_filename . $my_extension;
|
|
|
|
// If we are processing a chunked upload, source is the assembled file
|
|
// If it's a standard upload, it's the temp file
|
|
$my_path = $local_file_path ? $local_file_path : $_FILES['file_upload']['tmp_name'];
|
|
}
|
|
|
|
$my_upload_fullname = pathinfo($file_name_to_use, PATHINFO_BASENAME);
|
|
|
|
// Upload Logic
|
|
$upload_success = false;
|
|
|
|
if ($storage_type == 'local') {
|
|
// If local file path provided (Chunked), rename it to destination
|
|
if ($local_file_path) {
|
|
if (rename($local_file_path, $my_path)) {
|
|
$upload_success = true;
|
|
$final_url = $my_url . $my_final_file_name;
|
|
}
|
|
} else {
|
|
if (move_uploaded_file($_FILES['file_upload']['tmp_name'], $my_path)) {
|
|
$upload_success = true;
|
|
$final_url = $my_url . $my_final_file_name;
|
|
}
|
|
}
|
|
} elseif ($storage_type == 's3') {
|
|
try {
|
|
$args = [
|
|
'Bucket' => $bucket,
|
|
'Key' => $my_final_file_name,
|
|
'SourceFile' => $my_path,
|
|
];
|
|
|
|
if (!$disable_acl) {
|
|
$args['ACL'] = 'public-read';
|
|
}
|
|
|
|
$result = $s3->putObject($args);
|
|
|
|
// Cleanup temp file if it was a chunked upload
|
|
if ($local_file_path && file_exists($local_file_path)) {
|
|
unlink($local_file_path);
|
|
}
|
|
|
|
// Use S3 Object URL directly
|
|
$final_url = $result['ObjectURL'];
|
|
$upload_success = true;
|
|
} catch (Aws\S3\Exception\S3Exception $e) {
|
|
return '<font color="red">S3 Upload failed: ' . $e->getMessage() . '</font>';
|
|
}
|
|
}
|
|
|
|
if ($upload_success) {
|
|
// On success:
|
|
// obey custom shortname, if given:
|
|
$my_custom_shortname = '';
|
|
if (isset($_POST['custom_shortname']) && $_POST['custom_shortname'] != NULL) {
|
|
$my_custom_shortname = $_POST['custom_shortname'];
|
|
}
|
|
// change custom title, if given. Default is original filename, but if user provided one, use it:
|
|
$my_custom_title = $_POST['convert_filename'] . ': ' . $my_upload_fullname;
|
|
if (isset($_POST['custom_title']) && $_POST['custom_title'] != NULL) {
|
|
$my_custom_title = $_POST['custom_title'];
|
|
}
|
|
|
|
// let YOURLS create the link:
|
|
$my_short_url = yourls_add_new_link($final_url, $my_custom_shortname, $my_custom_title);
|
|
|
|
return '<font color="green">"' . $my_upload_fullname . '" successfully sent to ' . ($storage_type == 's3' ? 'S3' : 'Server') . '. Links:</font><br />' .
|
|
'Direct: <a href="' . $final_url . '" target="_blank">' . $final_url . '</a><br />' .
|
|
'Short: <a href="' . $my_short_url['shorturl'] . '" target="_blank">' . $my_short_url['shorturl'] . '</a>';
|
|
} else {
|
|
$error = isset($_FILES['file_upload']) ? $_FILES['file_upload']['error'] : 'Unknown error';
|
|
return '<font color="red">Upload failed, sorry! The error was ' . $error . '</font>';
|
|
}
|
|
}
|