HEX
Server: Apache/2.4.65 (Debian)
System: Linux localhost 6.9.12 #1 SMP Tue May 5 14:23:20 UTC 2026 x86_64
User: www-data (33)
PHP: 8.2.30
Disabled: NONE
Upload Files
File: /var/www/html/wp-content/plugins/wp-stateless/lib/classes/class-sync-non-media.php
<?php

/**
 * Need to improve workflow.
 * Maybe add a transient of few days to keep track of synced files.
 */

namespace wpCloud\StatelessMedia {

  if (!class_exists('wpCloud\StatelessMedia\SyncNonMedia')) {

    class SyncNonMedia {

      /**
       * Status constants 
       */
      const STATUS_SYNCED = 'synced';
      const STATUS_QUEUED = 'queued';
      const STATUS_COPIED = 'copied';
      const STATUS_MOVED = 'moved';

      /**
       * @var array
       * 
       * List of directories to scan for compatibility files.
       */
      private $registered_dir = array();

      public function __construct() {
        // Manual sync using sync tab.
        // Return files to be manually sync from sync tab.
        add_filter('sm:sync::nonMediaFiles', array($this, 'sync_non_media_files'));
        add_filter('sm:sync::queue_is_exists', array($this, 'queue_is_exists'), 10, 2);

        // register a dir to sync from sync tab
        add_action('sm:sync::register_dir', array($this, 'register_dir'));
        add_action('sm:sync::addFile', array($this, 'add_file'), 10, 2);
        add_action('sm:sync::removeFile', array($this, 'remove_file'));

        // Sync a file.
        add_action('sm:sync::syncFile', array($this, 'sync_file'), 10, 4);
        add_action('sm:sync::copyFile', array($this, 'copy_file'), 10, 2);
        add_action('sm:sync::moveFile', array($this, 'move_file'), 10, 2);
        add_action('sm:sync::deleteFile', array($this, 'delete_file'));
        add_action('sm:sync::deleteFiles', array($this, 'delete_files'));
        add_action('sm:sync::unregister_file', array($this, 'unregister_file'), 10, 1);
      }

      /**
       * Register dir to be sync from Sync tab.
       * @param
       * $dir: The directory to register
       */
      public function register_dir($dir) {
        if (!in_array($dir, $this->registered_dir)) {
          $this->registered_dir[] = $dir;
        }
      }
      /**
       * Add file to list of files to be sync from Sync tab.
       * Save the file path to database.
       * @param array $media
       * @param string $status
       */
      public function add_file($media, $status = self::STATUS_QUEUED) {
        ud_stateless_db()->update_non_library_file( (array) $media, $status);
      }

      /**
       * Sync the file to GCS.
       * @param $name: Relative path to upload dir.
       * @param $absolutePath: Full path of the file
       * @param bool $forced: Type: bool/int; Whether to force to move the file to GCS even it's already exists.
       *                      true: Check whether it's already synced or not in database.
       *                      2 (int): Force to overwrite on GCS
       * @param array $args
       * @return bool|void $media: Media object returned from client->add_media() method.
       * @throws: Exception File not found
       */
      public function sync_file($name, $absolutePath, $forced = false, $args = array()) {
        $sm_mode = ud_get_stateless_media()->get('sm.mode');

        $args = wp_parse_args($args, array(
          'ephemeral' => true, // whether to delete local file in ephemeral mode.
          'download'  => false, // whether to delete local file in ephemeral mode.
          'use_root'  => 0,
          'skip_db'   => false,
          'manual_sync' => false,
          'remove_from_queue' => false, // removes entry from queue table if both file is missing.
          'name_with_root' => true,
        ));

        $args = apply_filters('sm:sync::syncArgs', $args, $name, $absolutePath, $forced);

        if ( apply_filters('sm:sync::queue_is_exists', $name, self::STATUS_SYNCED) && !$forced) {
          return false;
        }

        $file_type = Utility::mimetype_from_extension(pathinfo($absolutePath, PATHINFO_EXTENSION));
        if (empty($this->client)) {
          $this->client = ud_get_stateless_media()->get_client();
        }

        if (is_wp_error($this->client)) {
          return;
        }

        $file_copied_from_gcs = false;
        $local_file_exists = file_exists($absolutePath);

        do_action('sm::pre::sync::nonMediaFiles', $name, $absolutePath); // , $media

        if (!$local_file_exists && ($args['download'] || ud_get_stateless_media()->get('sm.mode') !== 'ephemeral' || ud_get_stateless_media()->get('sm.mode') !== 'stateless')) {
          // Try get it and save
          $result_code = $this->client->get_media($name, true, $absolutePath);

          if ($result_code == 200) {
            $local_file_exists = true;
            $file_copied_from_gcs = true;
          }
        }

        if ($local_file_exists && !$file_copied_from_gcs && !$args['download']) {

          if ($sm_mode == 'stateless' && !wp_doing_ajax()) {
            global $gs_client;

            $gs_name = apply_filters('wp_stateless_file_name', $name, $args['name_with_root']);

            //Bucket
            $bucket = ud_get_stateless_media()->get('sm.bucket');

            $bucket = $gs_client->bucket($bucket);
            $object = $bucket->object($gs_name);
            $args = wp_parse_args($args, array(
              'use_root' => $args['use_root'],
              'force' => ($forced == 2),
              'name' => $name,
              'absolutePath' => $absolutePath,
              'mimeType' => $file_type,
              'metadata' => array(
                'child-of' => dirname($name),
                'file-hash' => md5($name),
                'source' => $args['source'] ?? '',
                'sourceVersion' => $args['source_version'] ?? '',
              ),
              'is_webp' => '',
            ));

            $args = apply_filters('wp_stateless_add_media_args', $args);

            /**
             * Updating object metadata, ACL, CacheControl and contentDisposition
             * @return media object
             */
            try {
              $mediaOptions = array(
                'cacheControl' => apply_filters('sm:item:cacheControl', ud_get_stateless_media()->get_default_cache_control(), $absolutePath),
                'contentDisposition' => apply_filters('sm:item:contentDisposition', null, $absolutePath)
              );

              if ( !defined('WP_STATELESS_SKIP_ACL_SET') || !WP_STATELESS_SKIP_ACL_SET) {
                $mediaOptions['predefinedAcl'] = 'publicRead';
              }

              $media = $object->update(array('metadata' => $args['metadata']) + $mediaOptions);
            } catch (\Throwable $th) {
              //throw $th;
            }
          } else {
            $media = $this->client->add_media(array(
              'use_root' => $args['use_root'],
              'name' => $name,
              'force' => ($forced == 2),
              'absolutePath' => $absolutePath,
              'cacheControl' => apply_filters('sm:item:cacheControl', ud_get_stateless_media()->get_default_cache_control(), $absolutePath), //@todo use cacheControl from settings page.
              'contentDisposition' => apply_filters('sm:item:contentDisposition', null, $absolutePath),
              'mimeType' => $file_type,
              'metadata' => array(
                'child-of' => dirname($name),
                'file-hash' => md5($name),
                'source' => $args['source'] ?? '',
                'sourceVersion' => $args['source_version'] ?? '',
              ),
            ));
          }

          // Addon can hook this function to modify database after manual sync done.
          do_action('sm::synced::nonMediaFiles', $name, $absolutePath, $media); // , $media

          // Ephemeral mode: we don't need the local version.
          if ($args['ephemeral'] == true && ud_get_stateless_media()->get('sm.mode') === 'ephemeral') {
            unlink($absolutePath);
          }

          if (!$args['skip_db']) {
            // add file_path to the file list.
            do_action('sm:sync::addFile', $media, self::STATUS_SYNCED);
          }
          return $media;
        } elseif (!$local_file_exists && $args['remove_from_queue']) {
          if (!$this->client->media_exists($name)) {
            do_action('sm:sync::removeFile', $name);
            if ($args['manual_sync']) {
              throw new UnprocessableException(sprintf(__("Both local and remote files are missing. File: %s ", ud_get_stateless_media()->domain), $name));
            }
          }
        }
      }

      /**
       * Generate list for manual sync using sync tab. Sync all register files, dir and passed files.
       * @param array $files - Additional files to sync.
       * @return array
       */
      public function sync_non_media_files($files = array()) {
        $upload_dir = wp_upload_dir();
        $files = array_merge( $files, apply_filters('wp_stateless_get_non_library_files', array(), '') );
        foreach ($this->registered_dir as $key => $dir) {
          $dir = $upload_dir['basedir'] . "/" . trim($dir, '/') . "/";
          if (is_dir($dir)) {
            // Getting all the files from dir recursively.
            $_files = Utility::get_files($dir);
            // validating and adding to the $files array.
            foreach ($_files as $id => $file) {
              if (!file_exists($file)) {
                continue;
              }

              $_file = str_replace(wp_normalize_path($upload_dir['basedir']), '', wp_normalize_path($file));
              if (!in_array($_file, $files)) {
                $files[] = trim($_file, '/');
              }
            }
          }
        }

        // $files = array_values(array_unique($files));
        return $files;
      }

      /**
       * Delete a file from GCS.
       *
       * @param $file
       * @param bool $force
       * @return bool
       */
      public function delete_file($file) {
        try {
          $file = trim($file, '/');
          if (empty($this->client)) {
            $this->client = ud_get_stateless_media()->get_client();
          }

          if (is_wp_error($this->client)) {
            return false;
          }
          // Removing file for GCS
          $this->client->remove_media($file, "", 0);
          do_action('sm:sync::removeFile', $file);
          return true;
        } catch (\Exception $e) {
          return false;
        }
      }

      /**
       * Remove registered files of specified dir from GCS.
       *
       * @param $dir
       * @return bool|void
       */
      public function delete_files($dir) {
        if (empty($this->client)) {
          $this->client = ud_get_stateless_media()->get_client();
        }

        if (is_wp_error($this->client)) {
          return;
        }

        // Removing the files one by one.
        $files = apply_filters('wp_stateless_get_non_library_files', array(), $dir);

        if ( !empty($files) ) {
          foreach ($files as $key => $file) {
            if (strpos($file, $dir) !== false) {
              $this->client->remove_media($file, "", 0);
              do_action('sm:sync::removeFile', $file);
            }
          }
        }

        return true;
      }

      /**
       * Checks whether a file is exist in database.
       * @param $file: Path of file relative to upload dir.
       * @param string $status: optional. queued|synced
       * @return mixed: non boolean true. number of item found in db.
       */
      public function queue_is_exists($file, $status = '') {
        return ud_stateless_db()->get_non_library_file_id($file, $status);
      }

      /**
       * Deletes a entry from database.
       * @param $file: Path of file relative to upload dir.
       * @return mixed
       */
      public function remove_file($file) {
        ud_stateless_db()->remove_non_library_file($file);
      }

      /**
       * Delete a file from GCS.
       * @param $old_file
       * @param $new_file
       * @param bool $force
       * @param string $status
       * @return bool
       */
      public function copy_file($old_file, $new_file, $force = false, $status = self::STATUS_COPIED) {
        try {
          if ( !$force && apply_filters('sm:sync::queue_is_exists', $new_file, $status) ) {
            return false;
          }

          $client = $this->get_gs_client();

          // Copying file on GCS
          $media = $client->copy_media($old_file, $new_file);

          do_action('sm:sync::addFile', $media, $status);

          return true;
        } catch (\Exception $e) {
          return false;
        }
      }

      /**
       * Delete a file from GCS.
       * @param $old_file
       * @param $new_file
       * @return bool
       */
      public function move_file($old_file, $new_file) {
        try {

          $this->copy_file($old_file, $new_file, true, self::STATUS_MOVED);
          $this->delete_file($old_file);

          return true;
        } catch (\Exception $e) {
          return false;
        }
      }

      /**
       * Get GS Client
       * @return mixed
       */
      public function get_gs_client() {
        if (empty($this->client)) {
          $this->client = ud_get_stateless_media()->get_client();
        }

        return $this->client;
      }

      /**
       * Remove file from DB after file deletion.
       * @param string $file
       */
      public function unregister_file($file) {
        $file = str_replace( ud_get_stateless_media()->get_gs_path(), '', $file);
        $file = str_replace( ud_get_stateless_media()->get_gs_host(), '', $file);

        $file = trim($file, '/');

        do_action('sm:sync::removeFile', $file);
      }
    }
  }
}