qdr-temporary-shared-storage.php 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235
  1. <?php
  2. /**
  3. * Plugin Name: QDR - Temporary Shared Storage (via REST API)
  4. * Plugin URI: https://www.quadarax.com/plugins/qdr-temporary-shared-storage
  5. * Description: Extends WordPress functionality for REST API interface to upload temporary media files and provide permalink to download to anybody who has permalink (via password protected download page)
  6. * Version: 1.0.0
  7. * Requires at least: 6.8.1
  8. * Requires PHP: 8.2
  9. * Author: Dalibor Votruba
  10. * Author URI: https://www.quadarax.com
  11. * License: GPL v2 or later
  12. * License URI: https://www.gnu.org/licenses/gpl-2.0.html
  13. * Text Domain: qdr-temporary-shared-storage
  14. * Domain Path: /languages
  15. * WC requires at least: 9.0
  16. * WC tested up to: 9.8
  17. */
  18. // If this file is called directly, abort.
  19. if (!defined('WPINC')) {
  20. die;
  21. }
  22. // Define plugin constants
  23. define('QDR_TSS_VERSION', '1.0.0');
  24. define('QDR_TSS_PLUGIN_DIR', plugin_dir_path(__FILE__));
  25. define('QDR_TSS_PLUGIN_URL', plugin_dir_url(__FILE__));
  26. define('QDR_TSS_PLUGIN_BASENAME', plugin_basename(__FILE__));
  27. define('QDR_TSS_UPLOADS_DIR', wp_upload_dir()['basedir'] . '/qdr-shared-storage/');
  28. define('QDR_TSS_UPLOADS_URL', wp_upload_dir()['baseurl'] . '/qdr-shared-storage/');
  29. define('QDR_TSS_PLUGIN_TABLE', 'qdr_temporary_shared_storage');
  30. /**
  31. * The core plugin class
  32. */
  33. class QDR_Temporary_Shared_Storage {
  34. /**
  35. * Plugin instance.
  36. *
  37. * @var QDR_Temporary_Shared_Storage
  38. */
  39. private static $instance = null;
  40. /**
  41. * Plugin settings.
  42. *
  43. * @var array
  44. */
  45. private $settings;
  46. /**
  47. * Get plugin instance.
  48. *
  49. * @return QDR_Temporary_Shared_Storage
  50. */
  51. public static function get_instance() {
  52. if (null === self::$instance) {
  53. self::$instance = new self();
  54. }
  55. return self::$instance;
  56. }
  57. /**
  58. * Constructor.
  59. */
  60. private function __construct() {
  61. // Load plugin textdomain for translations
  62. add_action('plugins_loaded', array($this, 'load_textdomain'));
  63. // Register activation and deactivation hooks
  64. register_activation_hook(__FILE__, array($this, 'activate'));
  65. register_deactivation_hook(__FILE__, array($this, 'deactivate'));
  66. // Initialize the plugin
  67. add_action('init', array($this, 'init'));
  68. // Add admin menu
  69. add_action('admin_menu', array($this, 'add_admin_menu'));
  70. // Register REST API routes
  71. add_action('rest_api_init', array($this, 'register_rest_routes'));
  72. // Register shortcode
  73. add_shortcode('qdr_tss_download', array($this, 'download_shortcode'));
  74. // Schedule cron job
  75. if (!wp_next_scheduled('qdr_tss_purge_expired_files')) {
  76. wp_schedule_event(time(), 'hourly', 'qdr_tss_purge_expired_files');
  77. }
  78. add_action('qdr_tss_purge_expired_files', array($this, 'purge_expired_files'));
  79. // Load settings
  80. $this->settings = get_option('qdr_tss_settings', array(
  81. 'api_key' => wp_generate_password(32, false),
  82. 'media_category' => 'shared-storage',
  83. 'max_upload_size' => 1073741824, // 1GB default
  84. ));
  85. // Add REST API check
  86. add_filter('rest_authentication_errors', array($this, 'check_api_key'));
  87. }
  88. /**
  89. * Load plugin textdomain.
  90. */
  91. public function load_textdomain() {
  92. load_plugin_textdomain('qdr-temporary-shared-storage', false, dirname(plugin_basename(__FILE__)) . '/languages');
  93. }
  94. /**
  95. * Plugin activation.
  96. */
  97. public function activate() {
  98. global $wpdb;
  99. // Create uploads directory if it doesn't exist
  100. if (!file_exists(QDR_TSS_UPLOADS_DIR)) {
  101. wp_mkdir_p(QDR_TSS_UPLOADS_DIR);
  102. // Create .htaccess file to protect direct access
  103. $htaccess_content = "# Disable directory browsing\nOptions -Indexes\n\n# Deny direct access to all files\n<FilesMatch \".*\">\nOrder Allow,Deny\nDeny from all\n</FilesMatch>";
  104. file_put_contents(QDR_TSS_UPLOADS_DIR . '.htaccess', $htaccess_content);
  105. }
  106. // Create custom table
  107. $table_name = $wpdb->prefix . QDR_TSS_PLUGIN_TABLE;
  108. $charset_collate = $wpdb->get_charset_collate();
  109. $sql = "CREATE TABLE $table_name (
  110. id bigint(20) NOT NULL AUTO_INCREMENT,
  111. media_id bigint(20) NOT NULL,
  112. original_file_name varchar(255) NOT NULL,
  113. description text NULL,
  114. message text NULL,
  115. active_from datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
  116. active_to datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
  117. password varchar(255) NOT NULL,
  118. reference varchar(255) NULL,
  119. upload_date datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
  120. download_count int DEFAULT 0 NOT NULL,
  121. last_download datetime DEFAULT '0000-00-00 00:00:00' NULL,
  122. file_path varchar(255) NOT NULL,
  123. file_size bigint(20) NOT NULL,
  124. PRIMARY KEY (id),
  125. KEY media_id (media_id),
  126. KEY reference (reference),
  127. KEY active_to (active_to)
  128. ) $charset_collate;";
  129. require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
  130. dbDelta($sql);
  131. // Default settings
  132. if (!get_option('qdr_tss_settings')) {
  133. add_option('qdr_tss_settings', array(
  134. 'api_key' => wp_generate_password(32, false),
  135. 'media_category' => 'shared-storage',
  136. 'max_upload_size' => 1073741824, // 1GB default
  137. ));
  138. }
  139. }
  140. /**
  141. * Plugin deactivation.
  142. */
  143. public function deactivate() {
  144. // Clear scheduled cron job
  145. wp_clear_scheduled_hook('qdr_tss_purge_expired_files');
  146. }
  147. /**
  148. * Initialize plugin.
  149. */
  150. public function init() {
  151. // Register scripts and styles
  152. add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
  153. // Create download page if it doesn't exist
  154. $download_page = get_page_by_path('shared-file-download');
  155. if (!$download_page) {
  156. $page_id = wp_insert_post(array(
  157. 'post_title' => __('Shared File Download', 'qdr-temporary-shared-storage'),
  158. 'post_content' => '[qdr_tss_download]',
  159. 'post_status' => 'publish',
  160. 'post_type' => 'page',
  161. 'post_name' => 'shared-file-download'
  162. ));
  163. }
  164. }
  165. /**
  166. * Enqueue admin scripts and styles.
  167. */
  168. public function enqueue_admin_scripts($hook) {
  169. // Only load on plugin admin pages
  170. if (strpos($hook, 'qdr-temporary-shared-storage') === false) {
  171. return;
  172. }
  173. wp_enqueue_style('qdr-tss-admin-css', QDR_TSS_PLUGIN_URL . 'assets/css/admin.css', array(), QDR_TSS_VERSION);
  174. wp_enqueue_script('qdr-tss-admin-js', QDR_TSS_PLUGIN_URL . 'assets/js/admin.js', array('jquery', 'jquery-ui-datepicker'), QDR_TSS_VERSION, true);
  175. // Add datepicker
  176. wp_enqueue_style('jquery-ui-datepicker');
  177. // Localize script
  178. wp_localize_script('qdr-tss-admin-js', 'qdr_tss', array(
  179. 'ajaxurl' => admin_url('admin-ajax.php'),
  180. 'nonce' => wp_create_nonce('qdr_tss_nonce'),
  181. 'strings' => array(
  182. 'confirm_delete' => __('Are you sure you want to delete this file?', 'qdr-temporary-shared-storage'),
  183. 'confirm_purge' => __('Are you sure you want to purge all expired files?', 'qdr-temporary-shared-storage')
  184. )
  185. ));
  186. }
  187. /**
  188. * Add admin menu items.
  189. */
  190. public function add_admin_menu() {
  191. // Add Shared Storage page under Media menu
  192. add_submenu_page(
  193. 'upload.php',
  194. __('Shared Storage', 'qdr-temporary-shared-storage'),
  195. __('Shared Storage', 'qdr-temporary-shared-storage'),
  196. 'manage_options',
  197. 'qdr-temporary-shared-storage',
  198. array($this, 'render_media_page')
  199. );
  200. // Add Settings page
  201. add_submenu_page(
  202. 'options-general.php',
  203. __('Shared Storage Settings', 'qdr-temporary-shared-storage'),
  204. __('Shared Storage', 'qdr-temporary-shared-storage'),
  205. 'manage_options',
  206. 'qdr-temporary-shared-storage-settings',
  207. array($this, 'render_settings_page')
  208. );
  209. }
  210. /**
  211. * Render the Media Shared Storage admin page.
  212. */
  213. public function render_media_page() {
  214. include_once QDR_TSS_PLUGIN_DIR . 'admin/media-page.php';
  215. }
  216. /**
  217. * Render the Settings admin page.
  218. */
  219. public function render_settings_page() {
  220. include_once QDR_TSS_PLUGIN_DIR . 'admin/settings-page.php';
  221. }
  222. /**
  223. * Register REST API routes.
  224. */
  225. public function register_rest_routes() {
  226. register_rest_route('qdr-tss/v1', '/media', array(
  227. 'methods' => 'POST',
  228. 'callback' => array($this, 'upload_media'),
  229. 'permission_callback' => array($this, 'check_rest_permission')
  230. ));
  231. register_rest_route('qdr-tss/v1', '/media/(?P<id>\d+)', array(
  232. 'methods' => 'GET',
  233. 'callback' => array($this, 'get_media'),
  234. 'permission_callback' => array($this, 'check_rest_permission')
  235. ));
  236. register_rest_route('qdr-tss/v1', '/media/(?P<id>\d+)', array(
  237. 'methods' => 'PUT',
  238. 'callback' => array($this, 'update_media'),
  239. 'permission_callback' => array($this, 'check_rest_permission')
  240. ));
  241. register_rest_route('qdr-tss/v1', '/media/(?P<id>\d+)', array(
  242. 'methods' => 'DELETE',
  243. 'callback' => array($this, 'delete_media'),
  244. 'permission_callback' => array($this, 'check_rest_permission')
  245. ));
  246. register_rest_route('qdr-tss/v1', '/media', array(
  247. 'methods' => 'GET',
  248. 'callback' => array($this, 'get_media_collection'),
  249. 'permission_callback' => array($this, 'check_rest_permission')
  250. ));
  251. // Chunked upload endpoints
  252. register_rest_route('qdr-tss/v1', '/upload/init', array(
  253. 'methods' => 'POST',
  254. 'callback' => array($this, 'init_chunked_upload'),
  255. 'permission_callback' => array($this, 'check_rest_permission')
  256. ));
  257. register_rest_route('qdr-tss/v1', '/upload/chunk', array(
  258. 'methods' => 'POST',
  259. 'callback' => array($this, 'upload_chunk'),
  260. 'permission_callback' => array($this, 'check_rest_permission')
  261. ));
  262. register_rest_route('qdr-tss/v1', '/upload/finalize', array(
  263. 'methods' => 'POST',
  264. 'callback' => array($this, 'finalize_chunked_upload'),
  265. 'permission_callback' => array($this, 'check_rest_permission')
  266. ));
  267. }
  268. /**
  269. * Check API key for REST API requests.
  270. */
  271. public function check_api_key($result) {
  272. // Only check for our namespace
  273. if (empty($result) && strpos($_SERVER['REQUEST_URI'], '/wp-json/qdr-tss/') !== false) {
  274. $headers = getallheaders();
  275. $api_key = isset($headers['X-Api-Key']) ? $headers['X-Api-Key'] : '';
  276. if ($api_key !== $this->settings['api_key']) {
  277. return new WP_Error(
  278. 'rest_forbidden',
  279. __('Invalid API key.', 'qdr-temporary-shared-storage'),
  280. array('status' => 403)
  281. );
  282. }
  283. }
  284. return $result;
  285. }
  286. /**
  287. * Check REST API permissions.
  288. */
  289. public function check_rest_permission() {
  290. $headers = getallheaders();
  291. $api_key = isset($headers['X-Api-Key']) ? $headers['X-Api-Key'] : '';
  292. return ($api_key === $this->settings['api_key']);
  293. }
  294. /**
  295. * Initialize chunked upload.
  296. */
  297. public function init_chunked_upload($request) {
  298. $params = $request->get_params();
  299. // Check required parameters
  300. if (!isset($params['original_file_name']) || !isset($params['file_size'])) {
  301. return new WP_Error('missing_params', __('Missing required parameters.', 'qdr-temporary-shared-storage'), array('status' => 400));
  302. }
  303. // Check max upload size
  304. if (intval($params['file_size']) > $this->settings['max_upload_size']) {
  305. return new WP_Error('file_too_large', __('File exceeds maximum upload size.', 'qdr-temporary-shared-storage'), array('status' => 400));
  306. }
  307. // Check total storage usage
  308. if (!$this->check_storage_limit(intval($params['file_size']))) {
  309. return new WP_Error('storage_limit', __('Storage limit exceeded.', 'qdr-temporary-shared-storage'), array('status' => 400));
  310. }
  311. // Create temporary upload directory
  312. $upload_id = uniqid();
  313. $upload_dir = QDR_TSS_UPLOADS_DIR . 'tmp/' . $upload_id;
  314. if (!file_exists(QDR_TSS_UPLOADS_DIR . 'tmp/')) {
  315. wp_mkdir_p(QDR_TSS_UPLOADS_DIR . 'tmp/');
  316. }
  317. wp_mkdir_p($upload_dir);
  318. return array(
  319. 'upload_id' => $upload_id,
  320. 'status' => 'initialized'
  321. );
  322. }
  323. /**
  324. * Upload a chunk.
  325. */
  326. public function upload_chunk($request) {
  327. $upload_id = $request->get_param('upload_id');
  328. $chunk_index = $request->get_param('chunk_index');
  329. $total_chunks = $request->get_param('total_chunks');
  330. if (!$upload_id || !isset($chunk_index) || !isset($total_chunks)) {
  331. return new WP_Error('missing_params', __('Missing required parameters.', 'qdr-temporary-shared-storage'), array('status' => 400));
  332. }
  333. $upload_dir = QDR_TSS_UPLOADS_DIR . 'tmp/' . $upload_id;
  334. // Check if the upload directory exists
  335. if (!file_exists($upload_dir)) {
  336. return new WP_Error('invalid_upload_id', __('Invalid upload ID.', 'qdr-temporary-shared-storage'), array('status' => 400));
  337. }
  338. // Handle file upload
  339. $files = $request->get_file_params();
  340. if (empty($files) || !isset($files['chunk'])) {
  341. return new WP_Error('no_file', __('No file uploaded.', 'qdr-temporary-shared-storage'), array('status' => 400));
  342. }
  343. $chunk_file = $files['chunk'];
  344. // Move uploaded chunk to temporary directory
  345. $chunk_path = $upload_dir . '/' . $chunk_index;
  346. move_uploaded_file($chunk_file['tmp_name'], $chunk_path);
  347. return array(
  348. 'upload_id' => $upload_id,
  349. 'chunk_index' => $chunk_index,
  350. 'status' => 'chunk_uploaded'
  351. );
  352. }
  353. /**
  354. * Finalize chunked upload.
  355. */
  356. public function finalize_chunked_upload($request) {
  357. global $wpdb;
  358. $params = $request->get_params();
  359. $upload_id = $params['upload_id'];
  360. $total_chunks = $params['total_chunks'];
  361. if (!$upload_id || !isset($total_chunks)) {
  362. return new WP_Error('missing_params', __('Missing required parameters.', 'qdr-temporary-shared-storage'), array('status' => 400));
  363. }
  364. // Check for required file metadata
  365. $required_fields = array('original_file_name', 'description', 'password');
  366. foreach ($required_fields as $field) {
  367. if (!isset($params[$field]) || empty($params[$field])) {
  368. return new WP_Error('missing_field', sprintf(__('Missing required field: %s', 'qdr-temporary-shared-storage'), $field), array('status' => 400));
  369. }
  370. }
  371. $upload_dir = QDR_TSS_UPLOADS_DIR . 'tmp/' . $upload_id;
  372. // Check that all chunks are present
  373. for ($i = 0; $i < $total_chunks; $i++) {
  374. if (!file_exists($upload_dir . '/' . $i)) {
  375. return new WP_Error('missing_chunks', __('Not all chunks have been uploaded.', 'qdr-temporary-shared-storage'), array('status' => 400));
  376. }
  377. }
  378. // Create final file directory
  379. $file_dir = QDR_TSS_UPLOADS_DIR . date('Y/m/d');
  380. if (!file_exists($file_dir)) {
  381. wp_mkdir_p($file_dir);
  382. }
  383. // Sanitize file name
  384. $original_file_name = sanitize_file_name($params['original_file_name']);
  385. $file_name = md5($original_file_name . time()) . '-' . $original_file_name;
  386. $file_path = $file_dir . '/' . $file_name;
  387. // Combine chunks
  388. $final_file = fopen($file_path, 'wb');
  389. for ($i = 0; $i < $total_chunks; $i++) {
  390. $chunk_path = $upload_dir . '/' . $i;
  391. $chunk = fopen($chunk_path, 'rb');
  392. stream_copy_to_stream($chunk, $final_file);
  393. fclose($chunk);
  394. unlink($chunk_path);
  395. }
  396. fclose($final_file);
  397. // Clean up temporary directory
  398. rmdir($upload_dir);
  399. // Get file size
  400. $file_size = filesize($file_path);
  401. // Set dates
  402. $active_from = !empty($params['active_from']) ? $params['active_from'] : current_time('mysql');
  403. $active_to = !empty($params['active_to']) ? $params['active_to'] : date('Y-m-d H:i:s', strtotime('+30 days'));
  404. // Generate a unique media ID and insert into database
  405. $media_id = time() . rand(1000, 9999);
  406. $table_name = $wpdb->prefix . QDR_TSS_PLUGIN_TABLE;
  407. $wpdb->insert(
  408. $table_name,
  409. array(
  410. 'media_id' => $media_id,
  411. 'original_file_name' => $original_file_name,
  412. 'description' => sanitize_textarea_field($params['description']),
  413. 'message' => isset($params['message']) ? sanitize_textarea_field($params['message']) : '',
  414. 'active_from' => $active_from,
  415. 'active_to' => $active_to,
  416. 'password' => wp_hash_password($params['password']),
  417. 'reference' => isset($params['reference']) ? sanitize_text_field($params['reference']) : '',
  418. 'upload_date' => current_time('mysql'),
  419. 'file_path' => str_replace(QDR_TSS_UPLOADS_DIR, '', $file_path),
  420. 'file_size' => $file_size
  421. ),
  422. array('%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d')
  423. );
  424. $download_url = site_url('shared-file-download/' . $media_id);
  425. $shortcode = '[qdr_tss_download id="' . $media_id . '"]';
  426. return array(
  427. 'media_id' => $media_id,
  428. 'permalink' => $download_url,
  429. 'shortcode' => $shortcode
  430. );
  431. }
  432. /**
  433. * Get single media details.
  434. */
  435. public function get_media($request) {
  436. global $wpdb;
  437. $id = $request->get_param('id');
  438. $table_name = $wpdb->prefix . QDR_TSS_PLUGIN_TABLE;
  439. $item = $wpdb->get_row($wpdb->prepare(
  440. "SELECT media_id, original_file_name, description, message, active_from, active_to, reference, upload_date, download_count, last_download, file_size
  441. FROM $table_name
  442. WHERE media_id = %d",
  443. $id
  444. ));
  445. if (!$item) {
  446. return new WP_Error('not_found', __('Media not found.', 'qdr-temporary-shared-storage'), array('status' => 404));
  447. }
  448. // Get additional information from WordPress attachment
  449. $attachment = get_post($id);
  450. if ($attachment && $attachment->post_type === 'attachment') {
  451. $item->attachment_url = wp_get_attachment_url($id);
  452. $item->attachment_title = $attachment->post_title;
  453. }
  454. return $item;
  455. }
  456. /**
  457. * Update media details.
  458. */
  459. public function update_media($request) {
  460. global $wpdb;
  461. $id = $request->get_param('id');
  462. $params = $request->get_params();
  463. $table_name = $wpdb->prefix . QDR_TSS_PLUGIN_TABLE;
  464. // Check if media exists in our custom table
  465. $item = $wpdb->get_row($wpdb->prepare(
  466. "SELECT id FROM $table_name WHERE media_id = %d",
  467. $id
  468. ));
  469. if (!$item) {
  470. return new WP_Error('not_found', __('Media not found.', 'qdr-temporary-shared-storage'), array('status' => 404));
  471. }
  472. // Check if the WordPress attachment exists
  473. $attachment = get_post($id);
  474. if (!$attachment || $attachment->post_type !== 'attachment') {
  475. return new WP_Error('attachment_not_found', __('WordPress attachment not found.', 'qdr-temporary-shared-storage'), array('status' => 404));
  476. }
  477. // Prepare update data for our custom table
  478. $update_data = array();
  479. $update_format = array();
  480. if (isset($params['message'])) {
  481. $update_data['message'] = sanitize_textarea_field($params['message']);
  482. $update_format[] = '%s';
  483. }
  484. if (isset($params['active_from'])) {
  485. $update_data['active_from'] = $params['active_from'];
  486. $update_format[] = '%s';
  487. }
  488. if (isset($params['active_to'])) {
  489. $update_data['active_to'] = $params['active_to'];
  490. $update_format[] = '%s';
  491. }
  492. if (isset($params['reference'])) {
  493. $update_data['reference'] = sanitize_text_field($params['reference']);
  494. $update_format[] = '%s';
  495. }
  496. if (empty($update_data)) {
  497. return new WP_Error('no_changes', __('No changes to update.', 'qdr-temporary-shared-storage'), array('status' => 400));
  498. }
  499. // Update in our custom table
  500. $wpdb->update(
  501. $table_name,
  502. $update_data,
  503. array('media_id' => $id),
  504. $update_format,
  505. array('%d')
  506. );
  507. // Update WordPress attachment if description is provided
  508. if (isset($params['description'])) {
  509. $description = sanitize_textarea_field($params['description']);
  510. $attachment_data = array(
  511. 'ID' => $id,
  512. 'post_excerpt' => $description
  513. );
  514. wp_update_post($attachment_data);
  515. // Also update our custom table
  516. $wpdb->update(
  517. $table_name,
  518. array('description' => $description),
  519. array('media_id' => $id),
  520. array('%s'),
  521. array('%d')
  522. );
  523. }
  524. // Get updated record
  525. $updated_item = $wpdb->get_row($wpdb->prepare(
  526. "SELECT media_id, original_file_name, description, message, active_from, active_to, reference, upload_date, download_count, last_download
  527. FROM $table_name
  528. WHERE media_id = %d",
  529. $id
  530. ));
  531. return $updated_item;
  532. }
  533. /**
  534. * Delete media.
  535. */
  536. public function delete_media($request) {
  537. global $wpdb;
  538. $id = $request->get_param('id');
  539. $table_name = $wpdb->prefix . QDR_TSS_PLUGIN_TABLE;
  540. // Get file path
  541. $item = $wpdb->get_row($wpdb->prepare(
  542. "SELECT id, file_path FROM $table_name WHERE media_id = %d",
  543. $id
  544. ));
  545. if (!$item) {
  546. return new WP_Error('not_found', __('Media not found.', 'qdr-temporary-shared-storage'), array('status' => 404));
  547. }
  548. // Delete file
  549. $file_path = QDR_TSS_UPLOADS_DIR . $item->file_path;
  550. if (file_exists($file_path)) {
  551. unlink($file_path);
  552. }
  553. // Delete from database
  554. $wpdb->delete($table_name, array('media_id' => $id), array('%d'));
  555. return array(
  556. 'deleted' => true,
  557. 'media_id' => $id
  558. );
  559. }
  560. /**
  561. * Get media collection.
  562. */
  563. public function get_media_collection($request) {
  564. global $wpdb;
  565. $table_name = $wpdb->prefix . QDR_TSS_PLUGIN_TABLE;
  566. // Parse query parameters
  567. $params = $request->get_params();
  568. $per_page = isset($params['per_page']) ? intval($params['per_page']) : 10;
  569. $page = isset($params['page']) ? intval($params['page']) : 1;
  570. $offset = ($page - 1) * $per_page;
  571. // Build query
  572. $query = "SELECT media_id, original_file_name, description, message, active_from, active_to, reference, upload_date, download_count, last_download, file_size
  573. FROM $table_name";
  574. $count_query = "SELECT COUNT(*) FROM $table_name";
  575. $where_conditions = array();
  576. $query_values = array();
  577. // Filter by original_file_name
  578. if (isset($params['original_file_name'])) {
  579. $where_conditions[] = "original_file_name LIKE %s";
  580. $query_values[] = '%' . $wpdb->esc_like($params['original_file_name']) . '%';
  581. }
  582. // Filter by reference
  583. if (isset($params['reference'])) {
  584. $where_conditions[] = "reference LIKE %s";
  585. $query_values[] = '%' . $wpdb->esc_like($params['reference']) . '%';
  586. }
  587. // Add WHERE conditions if any
  588. if (!empty($where_conditions)) {
  589. $query .= " WHERE " . implode(" AND ", $where_conditions);
  590. $count_query .= " WHERE " . implode(" AND ", $where_conditions);
  591. }
  592. // Add sorting
  593. if (isset($params['orderby'])) {
  594. $allowed_orderby_columns = array(
  595. 'media_id', 'original_file_name', 'description', 'active_from', 'active_to',
  596. 'reference', 'upload_date', 'download_count', 'last_download'
  597. );
  598. $orderby = in_array($params['orderby'], $allowed_orderby_columns) ? $params['orderby'] : 'upload_date';
  599. $order = (isset($params['order']) && strtoupper($params['order']) === 'ASC') ? 'ASC' : 'DESC';
  600. $query .= " ORDER BY $orderby $order";
  601. } else {
  602. $query .= " ORDER BY upload_date DESC";
  603. }
  604. // Add pagination
  605. $query .= " LIMIT %d OFFSET %d";
  606. $query_values[] = $per_page;
  607. $query_values[] = $offset;
  608. // Prepare and execute queries
  609. $items = $wpdb->get_results($wpdb->prepare($query, $query_values));
  610. $total_items = $wpdb->get_var($wpdb->prepare($count_query, array_slice($query_values, 0, -2)));
  611. // Prepare response
  612. $response = new WP_REST_Response($items);
  613. $response->header('X-WP-Total', $total_items);
  614. $response->header('X-WP-TotalPages', ceil($total_items / $per_page));
  615. return $response;
  616. }
  617. /**
  618. * Check storage limit.
  619. */
  620. private function check_storage_limit($new_file_size) {
  621. global $wpdb;
  622. $table_name = $wpdb->prefix . QDR_TSS_PLUGIN_TABLE;
  623. // Get total storage usage
  624. $total_size = $wpdb->get_var("SELECT SUM(file_size) FROM $table_name");
  625. $total_size = $total_size ? intval($total_size) : 0;
  626. // Check if adding new file would exceed limit
  627. return ($total_size + $new_file_size) <= $this->settings['max_upload_size'];
  628. }
  629. /**
  630. * Purge expired files.
  631. */
  632. public function purge_expired_files() {
  633. global $wpdb;
  634. $table_name = $wpdb->prefix . QDR_TSS_PLUGIN_TABLE;
  635. $current_time = current_time('mysql');
  636. // Get expired items
  637. $expired_items = $wpdb->get_results($wpdb->prepare(
  638. "SELECT id, media_id, file_path FROM $table_name WHERE active_to < %s",
  639. $current_time
  640. ));
  641. // Delete files and database records
  642. foreach ($expired_items as $item) {
  643. // Delete the WordPress attachment
  644. wp_delete_attachment($item->media_id, true);
  645. // Delete file if it still exists (in case the WordPress function didn't remove it)
  646. $file_path = QDR_TSS_UPLOADS_DIR . $item->file_path;
  647. if (file_exists($file_path)) {
  648. unlink($file_path);
  649. }
  650. // Delete from our custom table
  651. $wpdb->delete($table_name, array('id' => $item->id), array('%d'));
  652. }
  653. return count($expired_items);
  654. }
  655. /**
  656. * Download shortcode.
  657. */
  658. public function download_shortcode($atts) {
  659. global $wpdb;
  660. // Extract attributes
  661. $atts = shortcode_atts(array(
  662. 'id' => null,
  663. ), $atts, 'qdr_tss_download');
  664. // Get media ID from shortcode attribute or URL parameter
  665. $media_id = $atts['id'];
  666. if (!$media_id && isset($_GET['id'])) {
  667. $media_id = intval($_GET['id']);
  668. }
  669. // If no media ID, show form to enter media ID, password and reference
  670. if (!$media_id) {
  671. return $this->render_download_form();
  672. }
  673. $table_name = $wpdb->prefix . QDR_TSS_PLUGIN_TABLE;
  674. // Get media information
  675. $item = $wpdb->get_row($wpdb->prepare(
  676. "SELECT * FROM $table_name WHERE media_id = %d",
  677. $media_id
  678. ));
  679. if (!$item) {
  680. return '<div class="qdr-tss-error">' . __('File not found.', 'qdr-temporary-shared-storage') . '</div>';
  681. }
  682. // Check if file is active
  683. $current_time = current_time('mysql');
  684. if ($current_time < $item->active_from) {
  685. return '<div class="qdr-tss-error">' . __('This file is not yet available for download.', 'qdr-temporary-shared-storage') . '</div>';
  686. }
  687. if ($current_time > $item->active_to) {
  688. return '<div class="qdr-tss-error">' . __('This file has expired and is no longer available for download.', 'qdr-temporary-shared-storage') . '</div>';
  689. }
  690. // Check if form is submitted
  691. if (isset($_POST['qdr_tss_download_submit'])) {
  692. // Verify nonce
  693. if (!isset($_POST['qdr_tss_nonce']) || !wp_verify_nonce($_POST['qdr_tss_nonce'], 'qdr_tss_download')) {
  694. return '<div class="qdr-tss-error">' . __('Security check failed.', 'qdr-temporary-shared-storage') . '</div>';
  695. }
  696. // Verify password
  697. $password = isset($_POST['qdr_tss_password']) ? $_POST['qdr_tss_password'] : '';
  698. if (!wp_check_password($password, $item->password)) {
  699. return '<div class="qdr-tss-error">' . __('Invalid password.', 'qdr-temporary-shared-storage') . '</div>' . $this->render_download_form($media_id);
  700. }
  701. // Verify reference if set
  702. if (!empty($item->reference)) {
  703. $reference = isset($_POST['qdr_tss_reference']) ? $_POST['qdr_tss_reference'] : '';
  704. if ($reference !== $item->reference) {
  705. return '<div class="qdr-tss-error">' . __('Invalid reference.', 'qdr-temporary-shared-storage') . '</div>' . $this->render_download_form($media_id);
  706. }
  707. }
  708. // Update download count and last download time
  709. $wpdb->update(
  710. $table_name,
  711. array(
  712. 'download_count' => $item->download_count + 1,
  713. 'last_download' => current_time('mysql')
  714. ),
  715. array('id' => $item->id),
  716. array('%d', '%s'),
  717. array('%d')
  718. );
  719. // Initiate download
  720. $file_path = QDR_TSS_UPLOADS_DIR . $item->file_path;
  721. if (file_exists($file_path)) {
  722. // Set headers for download
  723. header('Content-Description: File Transfer');
  724. header('Content-Type: application/octet-stream');
  725. header('Content-Disposition: attachment; filename="' . $item->original_file_name . '"');
  726. header('Expires: 0');
  727. header('Cache-Control: must-revalidate');
  728. header('Pragma: public');
  729. header('Content-Length: ' . filesize($file_path));
  730. ob_clean();
  731. flush();
  732. readfile($file_path);
  733. exit;
  734. } else {
  735. return '<div class="qdr-tss-error">' . __('File not found on server.', 'qdr-temporary-shared-storage') . '</div>';
  736. }
  737. }
  738. // Show download form with media information
  739. $output = '<div class="qdr-tss-download-info">';
  740. $output .= '<h2>' . __('File Information', 'qdr-temporary-shared-storage') . '</h2>';
  741. $output .= '<p><strong>' . __('File Name:', 'qdr-temporary-shared-storage') . '</strong> ' . esc_html($item->original_file_name) . '</p>';
  742. if (!empty($item->description)) {
  743. $output .= '<p><strong>' . __('Description:', 'qdr-temporary-shared-storage') . '</strong> ' . esc_html($item->description) . '</p>';
  744. }
  745. if (!empty($item->message)) {
  746. $output .= '<p><strong>' . __('Message:', 'qdr-temporary-shared-storage') . '</strong> ' . esc_html($item->message) . '</p>';
  747. }
  748. $output .= '</div>';
  749. $output .= $this->render_download_form($media_id);
  750. return $output;
  751. }
  752. /**
  753. * Render download form.
  754. */
  755. private function render_download_form($media_id = null) {
  756. $form = '<div class="qdr-tss-download-form">';
  757. $form .= '<form method="post" action="">';
  758. // Add nonce for security
  759. $form .= wp_nonce_field('qdr_tss_download', 'qdr_tss_nonce', true, false);
  760. if (!$media_id) {
  761. $form .= '<div class="qdr-tss-form-group">';
  762. $form .= '<label for="qdr_tss_media_id">' . __('Media ID', 'qdr-temporary-shared-storage') . '</label>';
  763. $form .= '<input type="text" name="id" id="qdr_tss_media_id" required>';
  764. $form .= '</div>';
  765. } else {
  766. $form .= '<input type="hidden" name="id" value="' . esc_attr($media_id) . '">';
  767. }
  768. $form .= '<div class="qdr-tss-form-group">';
  769. $form .= '<label for="qdr_tss_password">' . __('Password', 'qdr-temporary-shared-storage') . '</label>';
  770. $form .= '<input type="password" name="qdr_tss_password" id="qdr_tss_password" required>';
  771. $form .= '</div>';
  772. $form .= '<div class="qdr-tss-form-group">';
  773. $form .= '<label for="qdr_tss_reference">' . __('Reference (if required)', 'qdr-temporary-shared-storage') . '</label>';
  774. $form .= '<input type="text" name="qdr_tss_reference" id="qdr_tss_reference">';
  775. $form .= '</div>';
  776. $form .= '<div class="qdr-tss-form-group">';
  777. $form .= '<button type="submit" name="qdr_tss_download_submit" class="qdr-tss-button">' . __('Download File', 'qdr-temporary-shared-storage') . '</button>';
  778. $form .= '</div>';
  779. $form .= '</form>';
  780. $form .= '</div>';
  781. return $form;
  782. }
  783. }
  784. // Initialize the plugin
  785. function qdr_temporary_shared_storage() {
  786. return QDR_Temporary_Shared_Storage::get_instance();
  787. }
  788. qdr_temporary_shared_storage();
  789. /**
  790. * Admin class to handle admin pages
  791. */
  792. class QDR_TSS_Admin {
  793. /**
  794. * Setup the admin hooks
  795. */
  796. public static function init() {
  797. add_action('wp_ajax_qdr_tss_get_items', array(__CLASS__, 'ajax_get_items'));
  798. add_action('wp_ajax_qdr_tss_edit_item', array(__CLASS__, 'ajax_edit_item'));
  799. add_action('wp_ajax_qdr_tss_delete_item', array(__CLASS__, 'ajax_delete_item'));
  800. add_action('wp_ajax_qdr_tss_save_settings', array(__CLASS__, 'ajax_save_settings'));
  801. add_action('wp_ajax_qdr_tss_purge_expired', array(__CLASS__, 'ajax_purge_expired'));
  802. }
  803. /**
  804. * AJAX handler for getting items
  805. */
  806. public static function ajax_get_items() {
  807. // Check nonce
  808. if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'qdr_tss_nonce')) {
  809. wp_send_json_error(array('message' => __('Security check failed.', 'qdr-temporary-shared-storage')));
  810. }
  811. global $wpdb;
  812. $table_name = $wpdb->prefix . QDR_TSS_PLUGIN_TABLE;
  813. // Parse parameters
  814. $per_page = isset($_POST['per_page']) ? intval($_POST['per_page']) : 10;
  815. $page = isset($_POST['page']) ? intval($_POST['page']) : 1;
  816. $offset = ($page - 1) * $per_page;
  817. // Build query
  818. $query = "SELECT * FROM $table_name";
  819. $count_query = "SELECT COUNT(*) FROM $table_name";
  820. $where_conditions = array();
  821. $query_values = array();
  822. // Search
  823. if (isset($_POST['search']) && !empty($_POST['search'])) {
  824. $search = $_POST['search'];
  825. $where_conditions[] = "(original_file_name LIKE %s OR reference LIKE %s)";
  826. $query_values[] = '%' . $wpdb->esc_like($search) . '%';
  827. $query_values[] = '%' . $wpdb->esc_like($search) . '%';
  828. }
  829. // Add WHERE conditions if any
  830. if (!empty($where_conditions)) {
  831. $query .= " WHERE " . implode(" AND ", $where_conditions);
  832. $count_query .= " WHERE " . implode(" AND ", $where_conditions);
  833. }
  834. // Add sorting
  835. $orderby = isset($_POST['orderby']) ? $_POST['orderby'] : 'upload_date';
  836. $order = isset($_POST['order']) ? $_POST['order'] : 'DESC';
  837. $allowed_columns = array(
  838. 'media_id', 'original_file_name', 'description', 'active_from', 'active_to',
  839. 'reference', 'upload_date', 'download_count', 'last_download'
  840. );
  841. if (!in_array($orderby, $allowed_columns)) {
  842. $orderby = 'upload_date';
  843. }
  844. $order = strtoupper($order) === 'ASC' ? 'ASC' : 'DESC';
  845. $query .= " ORDER BY $orderby $order";
  846. // Add pagination
  847. $query .= " LIMIT %d OFFSET %d";
  848. $query_values[] = $per_page;
  849. $query_values[] = $offset;
  850. // Prepare and execute queries
  851. $prepared_query = $wpdb->prepare($query, $query_values);
  852. $items = $wpdb->get_results($prepared_query);
  853. $prepared_count_query = count($query_values) > 2
  854. ? $wpdb->prepare($count_query, array_slice($query_values, 0, -2))
  855. : $count_query;
  856. $total_items = $wpdb->get_var($prepared_count_query);
  857. wp_send_json_success(array(
  858. 'items' => $items,
  859. 'total' => intval($total_items),
  860. 'total_pages' => ceil($total_items / $per_page)
  861. ));
  862. }
  863. /**
  864. * AJAX handler for editing an item
  865. */
  866. public static function ajax_edit_item() {
  867. // Check nonce
  868. if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'qdr_tss_nonce')) {
  869. wp_send_json_error(array('message' => __('Security check failed.', 'qdr-temporary-shared-storage')));
  870. }
  871. // Check required parameters
  872. if (!isset($_POST['id']) || empty($_POST['id'])) {
  873. wp_send_json_error(array('message' => __('Missing item ID.', 'qdr-temporary-shared-storage')));
  874. }
  875. global $wpdb;
  876. $table_name = $wpdb->prefix . QDR_TSS_PLUGIN_TABLE;
  877. $id = intval($_POST['id']);
  878. // Prepare update data
  879. $update_data = array();
  880. $update_format = array();
  881. if (isset($_POST['description'])) {
  882. $update_data['description'] = sanitize_textarea_field($_POST['description']);
  883. $update_format[] = '%s';
  884. }
  885. if (isset($_POST['message'])) {
  886. $update_data['message'] = sanitize_textarea_field($_POST['message']);
  887. $update_format[] = '%s';
  888. }
  889. if (isset($_POST['active_from'])) {
  890. $update_data['active_from'] = sanitize_text_field($_POST['active_from']);
  891. $update_format[] = '%s';
  892. }
  893. if (isset($_POST['active_to'])) {
  894. $update_data['active_to'] = sanitize_text_field($_POST['active_to']);
  895. $update_format[] = '%s';
  896. }
  897. if (isset($_POST['reference'])) {
  898. $update_data['reference'] = sanitize_text_field($_POST['reference']);
  899. $update_format[] = '%s';
  900. }
  901. if (empty($update_data)) {
  902. wp_send_json_error(array('message' => __('No changes to update.', 'qdr-temporary-shared-storage')));
  903. }
  904. // Update in database
  905. $result = $wpdb->update(
  906. $table_name,
  907. $update_data,
  908. array('id' => $id),
  909. $update_format,
  910. array('%d')
  911. );
  912. if ($result === false) {
  913. wp_send_json_error(array('message' => __('Failed to update item.', 'qdr-temporary-shared-storage')));
  914. }
  915. // Get updated record
  916. $item = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $id));
  917. wp_send_json_success(array(
  918. 'item' => $item,
  919. 'message' => __('Item updated successfully.', 'qdr-temporary-shared-storage')
  920. ));
  921. }
  922. /**
  923. * AJAX handler for deleting an item
  924. */
  925. public static function ajax_delete_item() {
  926. // Check nonce
  927. if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'qdr_tss_nonce')) {
  928. wp_send_json_error(array('message' => __('Security check failed.', 'qdr-temporary-shared-storage')));
  929. }
  930. // Check required parameters
  931. if (!isset($_POST['id']) || empty($_POST['id'])) {
  932. wp_send_json_error(array('message' => __('Missing item ID.', 'qdr-temporary-shared-storage')));
  933. }
  934. global $wpdb;
  935. $table_name = $wpdb->prefix . QDR_TSS_PLUGIN_TABLE;
  936. $id = intval($_POST['id']);
  937. // Get file path
  938. $item = $wpdb->get_row($wpdb->prepare(
  939. "SELECT file_path FROM $table_name WHERE id = %d",
  940. $id
  941. ));
  942. if (!$item) {
  943. wp_send_json_error(array('message' => __('Item not found.', 'qdr-temporary-shared-storage')));
  944. }
  945. // Delete file
  946. $file_path = QDR_TSS_UPLOADS_DIR . $item->file_path;
  947. if (file_exists($file_path)) {
  948. unlink($file_path);
  949. }
  950. // Delete from database
  951. $result = $wpdb->delete($table_name, array('id' => $id), array('%d'));
  952. if ($result === false) {
  953. wp_send_json_error(array('message' => __('Failed to delete item.', 'qdr-temporary-shared-storage')));
  954. }
  955. wp_send_json_success(array(
  956. 'message' => __('Item deleted successfully.', 'qdr-temporary-shared-storage')
  957. ));
  958. }
  959. /**
  960. * AJAX handler for saving settings
  961. */
  962. public static function ajax_save_settings() {
  963. // Check nonce
  964. if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'qdr_tss_nonce')) {
  965. wp_send_json_error(array('message' => __('Security check failed.', 'qdr-temporary-shared-storage')));
  966. }
  967. // Get current settings
  968. $settings = get_option('qdr_tss_settings', array());
  969. // Update settings
  970. if (isset($_POST['api_key'])) {
  971. $settings['api_key'] = sanitize_text_field($_POST['api_key']);
  972. }
  973. if (isset($_POST['media_category'])) {
  974. $settings['media_category'] = sanitize_text_field($_POST['media_category']);
  975. }
  976. if (isset($_POST['max_upload_size'])) {
  977. $settings['max_upload_size'] = intval($_POST['max_upload_size']);
  978. }
  979. // Save settings
  980. update_option('qdr_tss_settings', $settings);
  981. wp_send_json_success(array(
  982. 'message' => __('Settings saved successfully.', 'qdr-temporary-shared-storage')
  983. ));
  984. }
  985. /**
  986. * AJAX handler for purging expired files
  987. */
  988. public static function ajax_purge_expired() {
  989. // Check nonce
  990. if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'qdr_tss_nonce')) {
  991. wp_send_json_error(array('message' => __('Security check failed.', 'qdr-temporary-shared-storage')));
  992. }
  993. $plugin = qdr_temporary_shared_storage();
  994. $count = $plugin->purge_expired_files();
  995. wp_send_json_success(array(
  996. 'message' => sprintf(
  997. _n(
  998. '%d expired file has been purged.',
  999. '%d expired files have been purged.',
  1000. $count,
  1001. 'qdr-temporary-shared-storage'
  1002. ),
  1003. $count
  1004. )
  1005. ));
  1006. }
  1007. }
  1008. // Initialize admin class
  1009. QDR_TSS_Admin::init();
  1010. // Include admin pages
  1011. require_once QDR_TSS_PLUGIN_DIR . 'admin/media-page.php';
  1012. require_once QDR_TSS_PLUGIN_DIR . 'admin/settings-page.php';