HEX
Server: Apache
System: Linux cpanelx.inxs.ro 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: crowdandsafety (1041)
PHP: 8.1.34
Disabled: exec,passthru,shell_exec,system
Upload Files
File: //proc/self/cwd/wp-content/plugins/shortpixel-image-optimiser/class/Model/AiDataModel.php
<?php
namespace ShortPixel\Model;

if ( ! defined( 'ABSPATH' ) ) {
 exit; // Exit if accessed directly.
}

use ShortPixel\Helper\InstallHelper;
use ShortPixel\Helper\UtilHelper;
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Notices\NoticeController as Notice;

// Class to handle the Database Table Data, store AI relevant data etc. 
class AiDataModel
{
    protected $id; 
    protected $attach_id; 
    protected $type;

    protected static $models = []; 

    protected $original = [
        'alt' => null, 
        'caption' => null, 
        'description' => null, 
        'post_title' =>  null, 
        'filebase' => null, 
    ];

    protected $current = [
        'alt' => null,  
        'caption' => null, 
        'description' => null, 
        'post_title' => null, 
    ]; 

    protected $generated = [
        'alt' => null,  
        'caption' => null, 
        'description' => null,
        'post_title' => null, 
        'filebase' => null, 
    ]; 

    protected $status = 0;

    private $has_record = false; 
    private $has_generated = false; 
    private $current_is_set = false; 

    private $processable_status = 0;

    const TYPE_MEDIA = 1; 
    const TYPE_CUSTOM = 2; 

    // Status for the whole image, in the main table. 
    const AI_STATUS_NOTHING = 0;
    const AI_STATUS_GENERATED = 1; 

    // IsProcessable statii 
    const P_PROCESSABLE = 0; 
    const P_ALREADYDONE = 1;  // Data already there 
    const P_EXIFAI = 2;  // When Exif Flag forbids AI doing 
    const P_EXTENSION = 3; 
    const P_NOJOB = 4; 
    const P_NOFIELDS = 5; 

    // Descriptive status if certain field is not generated / left alone. 
    const F_STATUS_OK = 1; 
    const F_STATUS_EXCLUDESETTING = -3; 
    const F_STATUS_PREVENTOVERRIDE = -4;
    

    public function __construct($attach_id, $type = 'media')
    {
          $this->attach_id = $attach_id; 
          if ('media' == $type) // only this supported for now 
          {
             $this->type = self::TYPE_MEDIA; 
          }

          $this->fetchRecord($this->attach_id, $this->type);
    }

    protected function fetchRecord($attach_id, $type)
    {
           global $wpdb; 
           $tableName = self::getTableName();
           
           $sql = ' SELECT * FROM ' . $tableName . ' where attach_id = %d and post_type = %d';
           $sql = $wpdb->prepare($sql, $attach_id, $type); 

           $row = $wpdb->get_row($sql);  

        if (false === $row && strpos($wpdb->last_error, 'exist') !== false) {
			InstallHelper::checkTables();
            $this->has_record = false; 
			return false;
        }
        if (false == $row)
        {
            $this->has_record = false; 
            return; 
        }

        $originalData = $this->checkRowData($row->original_data); 
        $generatedData = $this->checkRowData($row->generated_data); 


        $this->id = $row->id; 
        $this->has_record = true; 
        $this->status = $row->status; 
        $this->original = array_merge($this->original, $originalData); 
        $this->generated = array_merge($this->generated, $generatedData); 

    }

    private function checkRowData($json)
    {
        $bool = UtilHelper::ValidateJSON($json);
        if (false === $bool)
        {
             return []; 
        }
        
        $data = json_decode($json); 

        return (array) $data;

    }

    /** Get all data needed to send API for generating AI texts, depending on settings. This includes all settings minus URL 
     * 
     * @return array{paramlist: array<string, array{context: mixed, chars: mixed}>, returndatalist: array<string, array<string, int>>} 
     */
    public function getOptimizeData($params = [])
    {
        $settings = (object) UtilHelper::getAiSettings($params); 

        $ignore_fields = []; 
        if (true === $settings->aiPreserve)
        {
            $currentData = $this->getCurrentData(); 
            $ignore_fields = array_diff(array_keys( array_filter($currentData) ), ['post_title']);
            // Exception via array_diff :: post_title always overwrite because it is always filled
        }

       // $fields = ['ai_gen_alt', 'ai_gen_caption', 'ai_gen_description', 'ai_gen_filename']; 
        $fields = ['alt', 'caption', 'description', 'filename', 'post_title']; 

        $paramlist = [
            'languages' => $settings->ai_language, 
            'context' => $settings->ai_general_context, 
        ]; 

        if (true === $settings->ai_use_post)
        {
            $parent_title = $this->getConnectedPostTitle(); 
            if (false !== $parent_title && false === is_null($parent_title))
            {
                $paramlist['use_parent_post_title'] = true; 
                $paramlist['parent_post_title'] = $parent_title;
            }
        }

        $returnDataList = []; 
        $field_status = false; // check if there are any fields to process / not all excluded. 

        foreach($fields as $field_name)
        {
            $api_name = $field_name; 

            switch($api_name)
            {
                case 'description': 
                    $api_name = 'image_description';
                break;
                case 'filename': 
                    $api_name = 'file';
                break; 
                case 'post_title': 
                    $api_name = 'title';
                break; 
            }


            if (false === $settings->{'ai_gen_' . $field_name})
            {
                $returnDataList[$field_name]['status'] = self::F_STATUS_EXCLUDESETTING;
                continue; 
            }
            elseif (true === in_array($field_name, $ignore_fields))
            {
                $returnDataList[$field_name]['status'] = self::F_STATUS_PREVENTOVERRIDE;
            }
            else
            {
                $paramlist[$api_name] = [
                        'context' => $settings->{'ai_' . $field_name . '_context'},
                        'chars' => $settings->{'ai_limit_' . $field_name . '_chars'},
                ];
                $returnDataList[$field_name]['status']  = self::F_STATUS_OK; 
                $field_status = true;
            }

           
        }

        if (false === $field_status)
        {
            $this->processable_status = self::P_NOJOB; 
        }

        return ['paramlist' => $paramlist, 'returndatalist' => $returnDataList]; 

    }

    public function handleNewData($data)
    {   
        // Save to Database
        foreach($data as $name => $value)
        {
             if ('original_filebase' == $name)
             {
                 $this->current['filebase'] = $value; 
             }
             else
             {
                $this->generated[$name] = $value;                 
             }

        }

        $this->setCurrentData();

        // New Data. 
        if (false === $this->has_record)
        {
            $this->original = $this->current; 
            
            $this->status = self::AI_STATUS_GENERATED;
            $this->updateRecord();
        }
        else
        {
            // Not sure if  just categorically deny this, or some smart updater ( with more risks ) 
            Log::addError('New AI Data already has an entry');
        }

        // Save to WordPress
        $this->updateWPPost($this->generated);
        $this->updateWpMeta($this->generated); 

    }

    protected function updateWPPost($data)
    {
        $post = get_post($this->attach_id); 
        $post_updated = false; 

        if (isset($data['caption']) && false !== $data['caption'] && false === is_numeric($data['caption']))
        {
            $post->post_excerpt = $data['caption'];
            $post_updated = true; 
        }

        if (isset($data['description']) && false !== $data['description'] && false === is_numeric($data['description']))
        {
            $post->post_content = $data['description'];
            $post_updated = true; 
        }

        if (isset($data['post_title']) && false !== $data['post_title'] && false === is_numeric($data['post_title']))
        {
             $post->post_title = $data['post_title'];
             $post_updated = true;
        }

        if (true === $post_updated)
        {
            wp_update_post($post);
        }
    }

    protected function updateWpMeta($data)
    {
        Log::addTemp('Update WpMeta', $data);
        if (isset($data['alt']) && false !== $data['alt'] && false === is_int($data['alt']))
        {
            $bool = update_post_meta($this->attach_id, '_wp_attachment_image_alt', $data['alt']);
        }
    }

    // Should return our results, from the AI only
    public function getGeneratedData()
    {
        return $this->generated;
    }

    public function getStatus()
    {
         return $this->status;
    }

    public function getAttachId()
    {
         return $this->attach_id;
    }

    public function currentIsDifferent()
    {
         $this->setCurrentData(); 

         $generated = array_filter($this->generated, [$this, 'mapWPVars'], ARRAY_FILTER_USE_KEY); 
         $current = array_filter($this->current, [$this, 'mapWPVars'], ARRAY_FILTER_USE_KEY);
         
         $diff = array_diff($generated, $current); 

         if (count($diff) > 0)
         {
             return true; 
         }
         return false; 
    }

    private function mapWPVars($key)
    {
         $fields = ['alt', 'caption', 'description']; 

         if (false === in_array($key, $fields))
         {
            return false; 
         }
         return true;
        
    }

    protected function getConnectedPostTitle()
    {
         $attach_id = $this->attach_id; 
         $post_parent = get_post_parent($attach_id); 
         if (! is_null($post_parent))
         {
            $post = get_post($post_parent); 
            if (false === is_null($post))
            {
                $post_title = $post->post_title; 
                return $post_title; 
            }
         }

         return false;
         
    }

    // Should return the current situation. If not stored in the database - or different from meta - uh something should be returned. 
    protected function setCurrentData()
    {
        $attach_id = $this->attach_id;      
        $current_alt = get_post_meta($attach_id, '_wp_attachment_image_alt', true);
        $post = get_post($attach_id); 

        $current_description = $post->post_content; 
        $current_caption = $post->post_excerpt; 
        $current_post_title = $post->post_title; 

        /*$this->current = [
             'alt' => $current_alt, 
             'description' => $current_description, 
             'caption' => $current_caption, 

        ]; */

        $this->current['alt'] = $current_alt; 
        $this->current['description'] = $current_description; 
        $this->current['caption'] = $current_caption;
        $this->current['post_title'] = $current_post_title; 

        $this->current_is_set = true; 

    }

    public function getCurrentData()
    {
          if (false === $this->current_is_set)
          {
             $this->setCurrentData(); 
          }

          return $this->current;
    }

    // This should return originals, or what the system thinks is the last user-generated content here. 
    public function getOriginalData()
    {
        return $this->original; 
    }

    // Check if the stored data still correlates to reality
    public function checkStoredData()
    {
        if (false === $this->has_record)
        {
            return true; 
        }

        $this->setCurrentData();
                 
    }

    /** Function to check if on this item there is something to AI 
     * 
     * @return boolean 
     */
    public function isProcessable()
    {
        if (true === $this->has_record)
        {
             $this->processable_status = SELF::P_ALREADYDONE;
             return false; 
        }

        // Stash here other conditions on top with && to build a big processable function 
        $processable = ( $this->isExifProcesssable() && $this->isExtensionIncluded() && $this->hasSomethingGeneratable() ) ? true : false; 
        return $processable; 
    }


    private function isExifProcesssable()
    {
        $fs = \wpSPIO()->filesystem(); 
        $imageModel = $fs->getMediaImage($this->attach_id); 

        if (false === $imageModel->isSomethingOptimized())
        {
            return true; 
        }

        $imageObj = $imageModel->getSomethingOptimized(); 
        

        $keepExif = $imageObj->getMeta('did_keepExif');

        // 2-3 are exif_ai combined settings with keep-exif. 0-1 are when default settings are used and unset / unused 
        if (in_array($keepExif, [0,1,2,3]))
        {
            return true; 
        }

        $this->processable_status = self::P_EXIFAI;
        return false; 

    }

    public function getProcessableReason($returnStatus = false )
    {
        $message = false; 
        
        if (true === $returnStatus)
        {
            return $this->processable_status;
        }

        switch($this->processable_status)
        {
            case self::P_PROCESSABLE:
                $message = __('AI is processable', 'shortpixel-image-optimiser');
            break; 
            case self::P_ALREADYDONE:
                $message = __('This image already has generated data', 'shortpixel-image-optimiser');
            break; 
            case self::P_EXIFAI:
                $message = __('Image Exif settings restrict AI usage', 'shortpixel-image-optimiser');
            break; 
            case self::P_EXTENSION:
                 $message = __('File Extension not supported', 'shortpixel-image-optimiser');
            break; 
            case self::P_NOJOB:
                $message = __('No fields to generate', 'shortpixel-image-optimiser'); 
            break; 
            default:
                 $message = sprintf(__('Status %s unknown', 'shortpixel-image-optimiser'), $this->processable_status);
            break; 
        }

        return $message;
    }

    protected function isExtensionIncluded()
    {
        $fs = \wpSPIO()->filesystem(); 
        $imageModel = $fs->getMediaImage($this->attach_id); 
        
        // Gif removed here, since we (temporarily don't support it)
        $extensions = ['png', 'jpeg', 'webp', 'jpg'];

        if (in_array($imageModel->getExtension(), $extensions))
        {
            return true; 
        }

        $this->processable_status = self::P_EXTENSION;
        return false; 
    }

    protected function hasSomethingGeneratable()
    {
        $optimizeData = $this->getOptimizeData(); 

        if (self::P_NOJOB === $this->processable_status)
        {
             return false; 

        }
        return true; 
    }

    public function isSomeThingGenerated()
    {
        if (false === $this->has_record)
        {
             return false; 
        }

        if (count(array_keys(array_filter($this->generated))) > 0)
        {
             return true; 
        }
        return false;
    }

    private static function getTableName()
    {
         global $wpdb; 
         return $wpdb->prefix . 'shortpixel_aipostmeta';
    }

    protected function updateRecord($data = [])
    {
        global $wpdb; 
        $is_new = is_null($this->id) ? true : false; 

        $fields = [
            'attach_id' => $this->attach_id, 
            'status' => $this->status, 
            'generated_data' => json_encode($this->generated), 
            'original_data' => json_encode($this->original), 
            'tsUpdated' => UtilHelper::timestampToDB(time()), 
        ];

        $format = ['%d', '%d', '%s', '%s', '%s'];

        if (false === $this->has_record)
        {
            $this->id = $wpdb->insert(self::getTableName(), $fields, $format); 
            $this->has_record = true; 
        }
        else
        {
            $wpdb->update(self::getTableName(),$fields, ['id' => $this->id],$format);
        }

    }

    public function migrate($data)
    {
        $updated = false; 
        if (false === is_array($data))
        {
            return false;
        }
        
        if (is_null($this->original['alt']))
        {
            $this->original['alt'] = $data['original_alt']; 
            $updated = true; 
        }
        if (is_null($this->generated['alt']))
        {
            $this->generated['alt'] = $data['result_alt'];
            $updated = true; 
        }
        
        if (true === $updated)
        {
            $this->status = self::AI_STATUS_GENERATED;
            $this->updateRecord();
            
        }

        return true;
    }

    public function revert()
    {   
        if (true === $this->has_record)
        {
            global $wpdb; 
            $wpdb->delete(self::getTableName(), ['id' => $this->id], ['%s']);

        }

        $this->updateWPPost($this->original);
        $this->updateWpMeta($this->original);
   
    }

    public static function getMostRecent()
    {
        global $wpdb; 
         $sql = 'SELECT attach_id FROM ' . self::getTableName() . ' order by tsUpdated desc limit 1'; 
         $attach_id = $wpdb->get_var($sql);         

        if (false === $attach_id)
        {
             return false; 
        }

        return new AiDataModel($attach_id);
    }

    
    public static function getModelByAttachment($attach_id, $type = 'media')
    {
        if (false === isset(self::$models[$attach_id]))
        {
             self::$models[$attach_id]  = new AiDataModel($attach_id, $type);
        }

        return self::$models[$attach_id];

    } 

    public static function flushModelCache($attach_id, $type = 'media')
    {
        if (isset(self::$models[$attach_id]))
        {
             unset(self::$models[$attach_id]);
        }
        else
        {
             Log::addTemp('Ai MODEL not found in cache!', $attach_id);
        }

    }




} // class