This commit is contained in:
2024-05-20 15:37:46 +03:00
commit 00b7dbd0b7
10404 changed files with 3285853 additions and 0 deletions

View File

@ -0,0 +1,557 @@
<?php
namespace ShortPixel\Model\File;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Helper\UtilHelper as UtilHelper;
/* Model for Directories
*
* For all low-level operations on directories
* Private model of FileSystemController. Please get your directories via there.
*
*/
class DirectoryModel extends \ShortPixel\Model
{
// Directory info
protected $path;
protected $name;
// Directory status
protected $exists = null;
protected $is_writable = null;
protected $is_readable = null;
protected $is_virtual = null;
protected $fields = array();
protected $new_directory_permission = 0755;
public static $TRUSTED_MODE = false;
/** Creates a directory model object. DirectoryModel directories don't need to exist on FileSystem
*
* When a filepath is given, it will remove the file part.
* @param $path String The Path
*/
public function __construct($path)
{
$fs = \wpSPIO()->filesystem();
// Test if path actually has someting, otherwise just bail.
if (strlen(trim($path)) == 0)
{
return false;
}
if ($fs->pathIsUrl($path))
{
$pathinfo = pathinfo($path);
if (isset($pathinfo['extension'])) // check if this is a file, remove the file information.
{
$path = $pathinfo['dirname'];
}
$this->is_virtual = true;
$this->is_readable = true; // assume
$this->exists = true;
}
// When in trusted mode prevent filesystem checks as much as possible.
if (true === self::$TRUSTED_MODE)
{
$this->exists = true;
$this->is_writable = true;
$this->is_readable = true;
}
// On virtual situation this would remove the slashes on :// , causing issues with offload et al.
if (false === $this->is_virtual)
$path = UtilHelper::spNormalizePath($path);
if (! $this->is_virtual() && ! is_dir($path) ) // path is wrong, *or* simply doesn't exist.
{
/* Test for file input.
* If pathinfo is fed a fullpath, it rips of last entry without setting extension, don't further trust.
* If it's a file extension is set, then trust.
*/
$pathinfo = pathinfo($path);
if (isset($pathinfo['extension']))
{
$path = $pathinfo['dirname'];
}
elseif (is_file($path))
$path = dirname($path);
}
if (! $this->is_virtual() && ! is_dir($path))
{
/* Check if realpath improves things. We support non-existing paths, which realpath fails on, so only apply on result.
Moved realpath to check after main pathinfo is set. Reason is that symlinked directories which don't include the WordPress upload dir will start to fail in file_model on processpath ( doesn't see it as a wp path, starts to try relative path). Not sure if realpath should be used anyhow in this model /BS
*/
$testpath = realpath($path);
if ($testpath)
$path = $testpath;
}
$this->path = trailingslashit($path);
// Basename doesn't work properly on non-latin ( cyrillic, greek etc ) directory names, returning the parent path instead.
$dir = new \SplFileInfo($path);
//basename($this->path);
$this->name = $dir->getFileName();
}
public function __toString()
{
return (string) $this->path;
}
/** Returns path *with* trailing slash
*
* @return String Path with trailing slash
*/
public function getPath()
{
return $this->path;
}
public function getModified()
{
return filemtime($this->path);
}
/**
* Get basename of the directory. Without path
*/
public function getName()
{
return $this->name;
}
public function exists()
{
if (is_null($this->exists))
{
$this->exists = file_exists($this->path) && is_dir($this->path);
}
return $this->exists;
}
public function is_writable()
{
if (is_null($this->is_writable))
{
$this->is_writable = is_writable($this->path);
}
return $this->is_writable;
}
public function is_readable()
{
if (is_null($this->is_readable))
{
$this->is_readable = is_readable($this->path);
}
return $this->is_readable;
}
public function is_virtual()
{
return $this->is_virtual;
}
/** Try to obtain the path, minus the installation directory.
* @return Mixed False if this didn't work, Path as string without basedir if it did. With trailing slash, without starting slash.
*/
public function getRelativePath()
{
// not used anywhere in directory.
// $upload_dir = wp_upload_dir(null, false);
$install_dir = get_home_path();
if($install_dir == '/') {
$install_dir = \wpSPIO()->filesystem()->getWPAbsPath();
}
$install_dir = trailingslashit($install_dir);
$path = $this->getPath();
// try to build relativePath without first slash.
$relativePath = str_replace($install_dir, '', $path);
if (is_dir( $install_dir . $relativePath) === false)
{
$test_path = $this->reverseConstructPath($path, $install_dir);
if ($test_path !== false)
{
$relativePath = $test_path;
}
else {
if($test_path = $this->constructUsualDirectories($path))
{
$relativePath = $test_path;
}
}
}
// If relativePath has less amount of characters, changes are this worked.
if (strlen($path) > strlen($relativePath))
{
return ltrim(trailingslashit($relativePath), '/');
}
return false;
}
private function reverseConstructPath($path, $install_path)
{
// Array value to reset index
$pathar = array_values(array_filter(explode('/', $path)));
$parts = array();
if (is_array($pathar))
{
// Reverse loop the structure until solid ground is found.
for ($i = (count($pathar)); $i > 0; $i--)
{
$parts[] = $pathar[$i - 1];
$testpath = implode('/', array_reverse($parts));
// if the whole thing exists
if (is_dir($install_path . $testpath) === true)
{
return $testpath;
}
}
}
return false;
}
/* Last Resort function to just reduce path to various known WorPress paths. */
private function constructUsualDirectories($path)
{
$pathar = array_values(array_filter(explode('/', $path))); // array value to reset index
$testpath = false;
if ( ($key = array_search('wp-content', $pathar)) !== false)
{
$testpath = implode('/', array_slice($pathar, $key));
}
elseif ( ($key = array_search('uploads', $pathar)) !== false)
{
$testpath = implode('/', array_slice($pathar, $key));
}
return $testpath;
}
/** Checks the directory into working order
* Tries to create directory if it doesn't exist
* Tries to fix file permission if writable is needed
* @param $check_writable Boolean Directory should be writable
*/
public function check($check_writable = false)
{
$permission = $this->getPermissionRecursive();
if ($permission === false) // if something wrong, return to default.
{
$permission = $this->new_directory_permission;
}
if (! $this->exists())
{
Log::addInfo('Directory does not exists. Try to create recursive ' . $this->path . ' with ' . $permission);
$result = @mkdir($this->path, $permission , true);
chmod ($this->path, $permission );
if (! $result)
{
$error = error_get_last();
Log::addWarn('MkDir failed: ' . $error['message'], array($error));
}
// reset.
$this->exists = null;
$this->is_readable = null;
$this->is_writable = null;
}
if ($this->exists() && $check_writable && ! $this->is_writable())
{
chmod($this->path, $permission);
if (! $this->is_writable()) // perhaps parent permission is no good.
{
chmod($this->path, $this->new_directory_permission);
}
}
if (! $this->exists())
{
Log::addInfo('Directory does not exist :' . $this->path);
return false;
}
if ($check_writable && !$this->is_writable())
{
Log::addInfo('Directory not writable :' . $this->path);
return false;
}
return true;
}
public function getPermissionRecursive()
{
$parent = $this->getParent();
if (! $parent->exists())
{
return $parent->getPermissionRecursive();
}
else
{
return $parent->getPermissions();
}
}
/* Get files from directory
* @returns Array|boolean Returns false if something wrong w/ directory, otherwise a files array of FileModel Object.
*/
public function getFiles($args = array())
{
$defaults = array(
'date_newer' => null,
'exclude_files' => null,
'include_files' => null,
);
$args = wp_parse_args($args, $defaults);
// if all filters are set to null, so point in checking those.
$has_filters = (count(array_filter($args)) > 0) ? true : false;
if ( ! $this->exists() || ! $this->is_readable() )
return false;
$fileArray = array();
if ($handle = opendir($this->path)) {
while (false !== ($entry = readdir($handle))) {
if ( ($entry != "." && $entry != "..") && ! is_dir($this->path . $entry) ) {
$fileObj = new FileModel($this->path . $entry);
if ($has_filters)
{
if ($this->fileFilter($fileObj,$args) === false)
{
$fileObj = null;
}
}
if (! is_null($fileObj))
$fileArray[] = $fileObj;
}
}
closedir($handle);
}
/*
if ($has_filters)
{
$fileArray = array_filter($fileArray, function ($file) use ($args) {
return $this->fileFilter($file, $args);
} );
} */
return $fileArray;
}
// @return boolean true if it should be kept in array, false if not.
private function fileFilter(FileModel $file, $args)
{
$filter = true;
if (! is_null($args['include_files']))
{
foreach($args['include_files'] as $inc)
{
// If any in included is true, filter is good for us.
$filter = false;
if (strpos( strtolower($file->getRawFullPath()), strtolower($inc) ) !== false)
{
$filter = true;
break;
}
}
}
if (! is_null($args['date_newer']))
{
$modified = $file->getModified();
if ($modified < $args['date_newer'] )
$filter = false;
}
if (! is_null($args['exclude_files']))
{
foreach($args['exclude_files'] as $ex)
{
if (strpos( strtolower($file->getRawFullPath()), strtolower($ex) ) !== false)
$filter = false;
}
}
return $filter;
}
/** Get subdirectories from directory
* * @returns Array|boolean Returns false if something wrong w/ directory, otherwise a files array of DirectoryModel Object.
*/
public function getSubDirectories()
{
if (! $this->exists() || ! $this->is_readable())
{
return false;
}
$dirIt = new \DirectoryIterator($this->path);
$dirArray = array();
foreach ($dirIt as $fileInfo)
{ // IsDot must go first here, or there is possiblity to run into openbasedir restrictions.
if (! $fileInfo->isDot() && $fileInfo->isDir() && $fileInfo->isReadable())
{
if ('ShortPixel\Model\File\DirectoryOtherMediaModel' == get_called_class())
{
$dir = new DirectoryOtherMediaModel($fileInfo->getRealPath());
}
else
{
$dir = new DirectoryModel($fileInfo->getRealPath());
}
if ($dir->exists())
$dirArray[] = $dir;
}
}
return $dirArray;
}
/** Check if this dir is a subfolder
* @param DirectoryModel The directoryObject that is tested as the parent */
public function isSubFolderOf(DirectoryModel $dir)
{
// the same path, is not a subdir of.
if ($this->getPath() === $dir->getPath())
return false;
// the main path must be followed from the beginning to be a subfolder.
if (strpos($this->getPath(), $dir->getPath() ) === 0)
{
return true;
}
return false;
}
//** Note, use sparingly, recursive function
public function getFolderSize()
{
// \wpSPIO()->filesystem()->getFilesRecursive($this)
$size = 0;
$files = $this->getFiles();
// GetFiles can return Boolean false on missing directory.
if (! is_array($files))
{
return $size;
}
foreach($files as $fileObj)
{
$size += $fileObj->getFileSize();
}
unset($files); //attempt at performance.
$subdirs = $this->getSubDirectories();
foreach($subdirs as $subdir)
{
$size += $subdir->getFolderSize();
}
return $size;
}
/** Get this paths parent */
public function getParent()
{
$path = $this->getPath();
$parentPath = dirname($path);
$parentDir = new DirectoryModel($parentPath);
return $parentDir;
}
public function getPermissions()
{
if (! $this->exists())
{
Log::addWarning('Directory not existing (fileperms): '. $this->getPath() );
return false;
}
$perms = fileperms($this->getPath());
if ($perms !== false)
{
return $perms;
}
else
return false;
}
public function delete()
{
return rmdir($this->getPath());
}
/** This will try to remove the whole structure. Use with care.
* This is mostly used to clear the backups.
*/
public function recursiveDelete()
{
if (! $this->exists() || ! $this->is_writable())
return false;
// This is a security measure to prevent unintended wipes.
$wpdir = \wpSPIO()->filesystem()->getWPUploadBase();
if (! $this->isSubFolderOf($wpdir))
return false;
$files = $this->getFiles();
$subdirs = $this->getSubDirectories();
foreach($files as $file)
$file->delete();
foreach($subdirs as $subdir)
$subdir->recursiveDelete();
$this->delete();
}
}

View File

@ -0,0 +1,637 @@
<?php
namespace ShortPixel\Model\File;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Notices\NoticeController as Notice;
use \ShortPixel\Model\File\DirectoryModel as DirectoryModel;
use \ShortPixel\Model\Image\ImageModel as ImageModel;
use ShortPixel\Controller\OptimizeController as OptimizeController;
use ShortPixel\Controller\OtherMediaController as OtherMediaController;
// extends DirectoryModel. Handles ShortPixel_meta database table
// Replacing main parts of shortpixel-folder
class DirectoryOtherMediaModel extends DirectoryModel
{
protected $id = -1; // if -1, this might not exist yet in Dbase. Null is not used, because that messes with isset
protected $name;
protected $status = 0;
protected $fileCount = 0; // inherent onreliable statistic in dbase. When insert / batch insert the folder count could not be updated, only on refreshFolder which is a relative heavy function to use on every file upload. Totals are better gotten from a stat-query, on request.
protected $updated = 0;
protected $created = 0;
protected $checked = 0;
protected $path_md5;
protected $is_nextgen = false;
protected $in_db = false;
protected $is_removed = false;
protected $last_message;
//protected $stats;
protected static $stats;
const DIRECTORY_STATUS_REMOVED = -1;
const DIRECTORY_STATUS_NORMAL = 0;
const DIRECTORY_STATUS_NEXTGEN = 1;
/** Path or Folder Object, from SpMetaDao
*
*/
public function __construct($path)
{
if (is_object($path)) // Load directly via Database object, this saves a query.
{
$folder = $path;
$path = $folder->path;
parent::__construct($path);
$this->loadFolder($folder);
}
else
{
parent::__construct($path);
$this->loadFolderbyPath($path);
}
}
public function get($name)
{
if (property_exists($this, $name))
return $this->$name;
return null;
}
public function set($name, $value)
{
if (property_exists($this, $name))
{
$this->$name = $value;
return true;
}
return null;
}
public static function getAllStats()
{
if (is_null(self::$stats))
{
global $wpdb;
$sql = 'SELECT SUM(CASE WHEN status = 2 OR status = -11 THEN 1 ELSE 0 END) optimized, SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) waiting, count(*) total, folder_id FROM ' . $wpdb->prefix . 'shortpixel_meta GROUP BY folder_id';
$result = $wpdb->get_results($sql, ARRAY_A);
$stats = array();
foreach($result as $rawdata)
{
$folder_id = $rawdata['folder_id'];
$data = array(
'optimized' => (int) $rawdata['optimized'],
'waiting' => (int) $rawdata['waiting'],
'total' => (int) $rawdata['total'],
);
$stats[$folder_id] = $data;
}
self::$stats = $stats;
}
return self::$stats;
}
public function getStats()
{
$stats = self::getAllStats(); // Querying all stats is more efficient than one-by-one
if (isset($stats[$this->id]))
{
return $stats[$this->id];
}
else {
global $wpdb;
$sql = "SELECT SUM(CASE WHEN status = 2 OR status = -11 THEN 1 ELSE 0 END) optimized, "
. "SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) waiting, count(*) total "
. "FROM " . $wpdb->prefix . "shortpixel_meta "
. "WHERE folder_id = %d";
$sql = $wpdb->prepare($sql, $this->id);
$res = $wpdb->get_row($sql, ARRAY_A);
if (is_array($res))
{
$result = array(
'optimized' => (int) $res['optimized'],
'waiting' => (int) $res['waiting'],
'total' => (int) $res['total'],
);
return $result;
}
else {
return false;
}
}
}
public function save()
{
// Simple Update
global $wpdb;
$data = array(
// 'id' => $this->id,
'status' => $this->status,
'file_count' => $this->fileCount,
'ts_updated' => $this->timestampToDB($this->updated),
'ts_checked' => $this->timestampToDB($this->checked),
'name' => $this->name,
'path' => $this->getPath(),
);
$format = array('%d', '%d', '%s', '%s', '%s', '%s');
$table = $wpdb->prefix . 'shortpixel_folders';
$is_new = false;
$result = false;
if ($this->in_db) // Update
{
$result = $wpdb->update($table, $data, array('id' => $this->id), $format);
}
else // Add new
{
// Fallback. In case some other process adds it. This happens with Nextgen.
if (true === $this->loadFolderByPath($this->getPath()))
{
$result = $wpdb->update($table, $data, array('id' => $this->id), $format);
}
else
{
$data['ts_created'] = $this->timestampToDB(time());
$this->id = $wpdb->insert($table, $data);
if ($this->id !== false)
{
$is_new = true;
$result = $this->id;
}
}
}
// reloading because action can create a new DB-entry, which will not be reflected (in id )
if ($is_new)
{
$this->loadFolderByPath($this->getPath());
}
return $result;
}
public function delete()
{
$id = $this->id;
if (! $this->in_db)
{
Log::addError('Trying to remove Folder without being in the database (in_db false) ' . $id, $this->getPath());
}
global $wpdb;
$otherMedia = OtherMediaController::getInstance();
// Remove all files from this folder that are not optimized.
$sql = "DELETE FROM " . $otherMedia->getMetaTable() . ' WHERE status <> 2 and folder_id = %d';
$sql = $wpdb->prepare($sql, $this->id);
$wpdb->query($sql);
// Check if there are any images left.
$sql = 'SELECT count(id) FROM ' . $otherMedia->getMetaTable() . ' WHERE folder_id = %d';
$sql = $wpdb->prepare($sql, $this->id);
$numImages = $wpdb->get_var($sql);
if ($numImages > 0)
{
$sql = 'UPDATE ' . $otherMedia->getFolderTable() . ' SET status = -1 where id = %d';
$sql = $wpdb->prepare($sql, $this->id);
$result = $wpdb->query($sql);
}
else
{
$sql = 'DELETE FROM ' . $otherMedia->getFolderTable() . ' where id = %d';
$sql = $wpdb->prepare($sql, $this->id);
$result = $wpdb->query($sql);
}
return $result;
}
public function isRemoved()
{
if ($this->is_removed)
return true;
else
return false;
}
/** Updates the updated variable on folder to indicating when the last file change was made
* @return boolean True if file were changed since last update, false if not
*/
public function updateFileContentChange()
{
if (! $this->exists() )
return false;
$old_time = $this->updated;
$time = $this->recurseLastChangeFile();
$this->updated = $time;
$this->save();
if ($old_time !== $time)
return true;
else
return false;
}
/** Crawls the folder and check for files that are newer than param time, or folder updated
* Note - last update timestamp is not updated here, needs to be done separately.
*/
public function refreshFolder($force = false)
{
if ($force === false)
{
$time = $this->updated;
}
else
{
$time = 0; //force refresh of the whole.
}
$stats = $this->getStats();
$total_before = $stats['total'];
if (! $this->checkDirectory(true))
{
Log::addWarn('Refreshing directory, something wrong in checkDirectory ' . $this->getPath());
return false;
}
if ($this->id <= 0)
{
Log::addWarn('FolderObj from database is not there, while folder seems ok ' . $this->getPath() );
return false;
}
elseif (! $this->exists())
{
$message = sprintf(__('Folder %s does not exist! ', 'shortpixel-image-optimiser'), $this->getPath());
$this->last_message = $message;
Notice::addError( $message );
return false;
}
elseif (! $this->is_writable())
{
$message = sprintf(__('Folder %s is not writeable. Please check permissions and try again.','shortpixel-image-optimiser'),$this->getPath());
$this->last_message = $message;
Notice::addWarning( $message );
return false;
}
$fs = \wpSPIO()->filesystem();
$filter = ($time > 0) ? array('date_newer' => $time) : array();
$filter['exclude_files'] = array('.webp', '.avif');
$filter['include_files'] = ImageModel::PROCESSABLE_EXTENSIONS;
$files = $fs->getFilesRecursive($this, $filter);
\wpSPIO()->settings()->hasCustomFolders = time(); // note, check this against bulk when removing. Custom Media Bulk depends on having a setting.
$result = $this->addImages($files);
// Reset stat.
unset(self::$stats[$this->id]);
$stats = $this->getStats();
$this->fileCount = $stats['total'];
$this->checked = time();
$this->save();
$stats['new'] = $stats['total'] - $total_before;
return $stats;
}
/** Check if a directory is allowed. Directory can't be media library, outside of root, or already existing in the database
* @param $silent If not allowed, don't generate notices.
*
*/
public function checkDirectory($silent = false)
{
$fs = \wpSPIO()->filesystem();
$rootDir = $fs->getWPFileBase();
$backupDir = $fs->getDirectory(SHORTPIXEL_BACKUP_FOLDER);
$otherMediaController = OtherMediaController::getInstance();
if (! $this->exists())
{
$message = __('Could not be added, directory not found: ' . $path ,'shortpixel-image-optimiser');
$this->last_message = $message;
if (false === $silent)
{
Notice::addError($message);
}
return false;
}
elseif (! $this->isSubFolderOf($rootDir) && $this->getPath() != $rootDir->getPath() )
{
$message = sprintf(__('The %s folder cannot be processed as it\'s not inside the root path of your website (%s).','shortpixel-image-optimiser'),$this->getPath(), $rootDir->getPath());
$this->last_message = $message;
if (false === $silent)
{
Notice::addError( $message );
}
return false;
}
elseif($this->isSubFolderOf($backupDir) || $this->getPath() == $backupDir->getPath() )
{
$message = __('This folder contains the ShortPixel Backups. Please select a different folder.','shortpixel-image-optimiser');
$this->last_message = $message;
if (false === $silent)
{
Notice::addError( $message );
}
return false;
}
elseif( $otherMediaController->checkIfMediaLibrary($this) )
{
$message = __('This folder contains Media Library images. To optimize Media Library images please go to <a href="upload.php?mode=list">Media Library list view</a> or to <a href="upload.php?page=wp-short-pixel-bulk">ShortPixel Bulk page</a>.','shortpixel-image-optimiser');
$this->last_message = $message;
if (false === $silent)
{
Notice::addError($message);
}
return false;
}
elseif (! $this->is_writable())
{
$message = sprintf(__('Folder %s is not writeable. Please check permissions and try again.','shortpixel-image-optimiser'),$this->getPath());
$this->last_message = $message;
if (false === $silent)
{
Notice::addError( $message );
}
return false;
}
else
{
$folders = $otherMediaController->getAllFolders();
foreach($folders as $folder)
{
if ($this->isSubFolderOf($folder))
{
if (false === $silent)
{
Notice::addError(sprintf(__('This folder is a subfolder of an already existing Other Media folder. Folder %s can not be added', 'shortpixel-image-optimiser'), $this->getPath() ));
}
return false;
}
}
}
return true;
}
/*
public function getFiles($args = array())
{
// Check if this directory if not forbidden.
if (! $this->checkDirectory(true))
{
return array();
}
return parent::getFiles($args);
}
*/
/* public function getSubDirectories()
{
$dirs = parent::getSubDirectories();
$checked = array();
foreach($dirs as $dir)
{
if ($dir->checkDirectory(false))
{
$checked[] = $dir;
}
else
{
Log::addDebug('Illegal directory' . $dir->getPath());
}
}
return $checked;
}
*/
private function recurseLastChangeFile($mtime = 0)
{
$ignore = array('.','..');
// Directories without read rights should not be checked at all.
if (! $this->is_readable())
return $mtime;
$path = $this->getPath();
$files = scandir($path);
// no files, nothing to update.
if (! is_array($files))
{
return $mtime;
}
$files = array_diff($files, $ignore);
$mtime = max($mtime, filemtime($path));
foreach($files as $file) {
$filepath = $path . $file;
if (is_dir($filepath)) {
$mtime = max($mtime, filemtime($filepath));
$subDirObj = new DirectoryOtherMediaModel($filepath);
$subdirtime = $subDirObj->recurseLastChangeFile($mtime);
if ($subdirtime > $mtime)
$mtime = $subdirtime;
}
}
return $mtime;
}
private function timestampToDB($timestamp)
{
if ($timestamp == 0) // when adding / or empty.
$timestamp = time();
return date("Y-m-d H:i:s", $timestamp);
}
private function DBtoTimestamp($date)
{
if (is_null($date))
{
$timestamp = time();
}
else {
$timestamp =strtotime($date);
}
return $timestamp;
}
/** This function is called by OtherMediaController / RefreshFolders. Other scripts should not call it
* @public
* @param Array of CustomMediaImageModel stubs.
*/
public function addImages($files) {
global $wpdb;
if ( apply_filters('shortpixel/othermedia/addfiles', true, $files, $this) === false)
{
return false;
}
$values = array();
$optimizeControl = new OptimizeController();
$otherMediaControl = OtherMediaController::getInstance();
$activeFolders = $otherMediaControl->getActiveDirectoryIDS();
$fs = \wpSPIO()->filesystem();
foreach($files as $fileObj)
{
// Note that load is set to false here.
$imageObj = $fs->getCustomStub($fileObj->getFullPath(), false);
// image already exists
if ($imageObj->get('in_db') == true)
{
// Load meta to make it check the folder_id.
$imageObj->loadMeta();
// Check if folder id is something else. This might indicate removed or inactive folders.
// If in inactive folder, move to current active.
if ($imageObj->get('folder_id') !== $this->id)
{
if (! in_array($imageObj->get('folder_id'), $activeFolders) )
{
$imageObj->setFolderId($this->id);
$imageObj->saveMeta();
}
}
// If in Db, but not optimized and autoprocess is on; add to queue for optimizing
if (\wpSPIO()->env()->is_autoprocess && $imageObj->isProcessable())
{
$optimizeControl->addItemToQueue($imageObj);
}
continue;
}
elseif ($imageObj->isProcessable()) // Check strict on Processable here.
{
$imageObj->setFolderId($this->id);
$imageObj->saveMeta();
if (\wpSPIO()->env()->is_autoprocess)
{
$optimizeControl->addItemToQueue($imageObj);
}
}
else {
}
}
}
private function loadFolderByPath($path)
{
//$folders = self::getFolders(array('path' => $path));
global $wpdb;
$sql = 'SELECT * FROM ' . $wpdb->prefix . 'shortpixel_folders where path = %s ';
$sql = $wpdb->prepare($sql, $path);
$folder = $wpdb->get_row($sql);
if (! is_object($folder))
return false;
else
{
$this->loadFolder($folder);
$this->in_db = true; // exists in database
return true;
}
}
/** Loads from database into model, the extra data of this model. */
private function loadFolder($folder)
{
// $class = get_class($folder);
// Setters before action
$this->id = $folder->id;
if ($this->id > 0)
$this->in_db = true;
$this->updated = property_exists($folder,'ts_updated') ? $this->DBtoTimestamp($folder->ts_updated) : time();
$this->created = property_exists($folder,'ts_created') ? $this->DBtoTimestamp($folder->ts_created) : time();
$this->checked = property_exists($folder,'ts_checked') ? $this->DBtoTimestamp($folder->ts_checked) : time();
$this->fileCount = property_exists($folder,'file_count') ? $folder->file_count : 0; // deprecated, do not rely on.
$this->status = $folder->status;
if (strlen($folder->name) == 0)
$this->name = basename($folder->path);
else
$this->name = $folder->name;
do_action('shortpixel/othermedia/folder/load', $this->id, $this);
// Making conclusions after action.
if ($this->status == -1)
$this->is_removed = true;
if ($this->status == self::DIRECTORY_STATUS_NEXTGEN)
{
$this->is_nextgen = true;
}
}
}

View File

@ -0,0 +1,889 @@
<?php
namespace ShortPixel\Model\File;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
use ShortPixel\ShortPixelLogger\ShortPixelLogger as Log;
use ShortPixel\Helper\UtilHelper as UtilHelper;
/* FileModel class.
*
*
* - Represents a -single- file.
* - Can handle any type
* - Usually controllers would use a collection of files
* - Meant for all low-level file operations and checks.
* - Every file can have a backup counterpart.
*
*/
class FileModel extends \ShortPixel\Model
{
// File info
protected $fullpath = null;
protected $rawfullpath = null;
protected $filename = null; // filename + extension
protected $filebase = null; // filename without extension
protected $directory = null;
protected $extension = null;
protected $mime = null;
protected $permissions = null;
protected $filesize = null;
// File Status
protected $exists = null;
protected $is_writable = null;
protected $is_directory_writable = null;
protected $is_readable = null;
protected $is_file = null;
protected $is_virtual = false;
protected $virtual_status = null;
protected $status;
protected $backupDirectory;
const FILE_OK = 1;
const FILE_UNKNOWN_ERROR = 2;
public static $TRUSTED_MODE = false;
// Constants for is_virtual . Virtual Remote is truly a remote file, not writable from machine. Stateless means it looks remote, but it's a protocol-based filesystem remote or not - that will accept writes / is_writable. Stateless also mean performance issue since it can't be 'translated' to a local path. All communication happens over http wrapper, so check should be very limited.
public static $VIRTUAL_REMOTE = 1;
public static $VIRTUAL_STATELESS = 2;
/** Creates a file model object. FileModel files don't need to exist on FileSystem */
public function __construct($path)
{
$this->rawfullpath = $path;
if (is_null($path))
{
Log::addWarn('FileModel: Loading null path! ');
return false;
}
if (strlen($path) > 0)
$path = trim($path);
$this->fullpath = $path;
$this->checkTrustedMode();
$fs = \wpSPIO()->filesystem();
if ($fs->pathIsUrl($path)) // Asap check for URL's to prevent remote wrappers from running.
{
$this->UrlToPath($path);
}
}
/* Get a string representation of file, the fullpath
* Note - this might be risky, without processedpath, in cases.
* @return String Full path processed or unprocessed.
*/
public function __toString()
{
return (string) $this->fullpath;
}
protected function setFileInfo()
{
$processed_path = $this->processPath($this->fullpath);
if ($processed_path !== false)
$this->fullpath = $processed_path; // set processed path if that went alright
$info = $this->mb_pathinfo($this->fullpath);
// Todo, maybe replace this with splFileINfo.
if ($this->is_file()) // only set fileinfo when it's an actual file.
{
$this->filename = isset($info['basename']) ? $info['basename'] : null; // filename + extension
$this->filebase = isset($info['filename']) ? $info['filename'] : null; // only filename
$this->extension = isset($info['extension']) ? strtolower($info['extension']) : null; // only (last) extension
}
}
/** Call when file status changed, so writable / readable / exists are not reliable anymore */
public function resetStatus()
{
$this->is_writable = null;
$this->is_directory_writable = null;
$this->is_readable = null;
$this->is_file = null;
$this->exists = null;
$this->is_virtual = null;
$this->filesize = null;
$this->permissions = null;
}
/**
* @param $forceCheck Forces a filesystem check instead of using cached. Use very sparingly. Implemented for retina on trusted mode.
*/
public function exists($forceCheck = false)
{
if (true === $forceCheck || is_null($this->exists))
{
if (true === $this->fileIsRestricted($this->fullpath))
{
$this->exists = false;
}
else {
$this->exists = (@file_exists($this->fullpath) && is_file($this->fullpath));
}
}
$this->exists = apply_filters('shortpixel_image_exists', $this->exists, $this->fullpath, $this); //legacy
$this->exists = apply_filters('shortpixel/file/exists', $this->exists, $this->fullpath, $this);
return $this->exists;
}
public function is_writable()
{
// Return when already asked / Stateless might set this
if (! is_null($this->is_writable))
{
return $this->is_writable;
}
elseif ($this->is_virtual())
{
$this->is_writable = false; // can't write to remote files
}
elseif (is_null($this->is_writable))
{
if ($this->exists())
{
$this->is_writable = @is_writable($this->fullpath);
}
else // quite expensive check to see if file is writable.
{
$res = $this->create();
$this->delete();
$this->is_writable = $res;
}
}
return $this->is_writable;
}
public function is_directory_writable()
{
// Return when already asked / Stateless might set this
if (! is_null($this->is_directory_writable))
{
return $this->is_directory_writable;
}
elseif ($this->is_virtual())
{
$this->is_directory_writable = false; // can't write to remote files
}
elseif (is_null($this->is_directory_writable))
{
$directory = $this->getFileDir();
if (is_object($directory) && $directory->exists())
{
$this->is_directory_writable = $directory->is_writable();
}
else {
$this->is_directory_writable = false;
}
}
return $this->is_directory_writable;
}
public function is_readable()
{
if (is_null($this->is_readable))
$this->is_readable = @is_readable($this->fullpath);
return $this->is_readable;
}
// A file is virtual when the file is remote with URL and no local alternative is present.
public function is_virtual()
{
if ( is_null($this->is_virtual))
$this->is_virtual = false; // return bool
return $this->is_virtual;
}
/* Function checks if path is actually a file. This can be used to check possible confusion if a directory path is given to filemodel */
public function is_file()
{
if ($this->is_virtual()) // don't look further when virtual
{
$this->is_file = true;
return $this->is_file;
}
elseif (is_null($this->is_file))
{
if ($this->exists())
{
if (basename($this->fullpath) == '..' || basename($this->fullpath) == '.')
$this->is_file = false;
else
$this->is_file = is_file($this->fullpath);
}
else // file can not exist, but still have a valid filepath format. In that case, if file should return true.
{
/* if file does not exist on disk, anything can become a file ( with/ without extension, etc). Meaning everything non-existing is a potential file ( or directory ) until created. */
if (basename($this->fullpath) == '..' || basename($this->fullpath) == '.') // don't see this as file.
{
$this->is_file = false;
}
else if (! file_exists($this->fullpath) && ! is_dir($this->fullpath))
{
$this->is_file = true;
}
else //if (! is_file($this->fullpath)) // can be a non-existing directory. /
{
$this->is_file = false;
}
}
}
return $this->is_file;
}
public function getModified()
{
return filemtime($this->fullpath);
}
public function hasBackup()
{
$directory = $this->getBackupDirectory();
if (! $directory)
return false;
$backupFile = $directory . $this->getBackupFileName();
if (file_exists($backupFile) && ! is_dir($backupFile) )
return true;
else {
return false;
}
}
/** Tries to retrieve an *existing* BackupFile. Returns false if not present.
* This file might not be writable.
* To get writable directory reference to backup, use FileSystemController
*/
public function getBackupFile()
{
if ($this->hasBackup())
return new FileModel($this->getBackupDirectory() . $this->getBackupFileName() );
else
return false;
}
/** Function returns the filename for the backup. This is an own function so it's possible to manipulate backup file name if needed, i.e. conversion or enumeration */
public function getBackupFileName()
{
return $this->getFileName();
}
/** Returns the Directory Model this file resides in
*
* @return DirectoryModel Directorymodel Object
*/
public function getFileDir()
{
$fullpath = $this->getFullPath(); // triggers a file lookup if needed.
// create this only when needed.
if (is_null($this->directory) && strlen($fullpath) > 0)
{
// Feed to full path to DirectoryModel since it checks if input is file, or dir. Using dirname here would cause errors when fullpath is already just a dirpath ( faulty input )
$this->directory = new DirectoryModel($fullpath);
}
return $this->directory;
}
public function getFileSize()
{
if (! is_null($this->filesize))
{
return $this->filesize;
}
elseif ($this->exists() && false === $this->is_virtual() )
{
$this->filesize = filesize($this->fullpath);
return $this->filesize;
}
elseif (true === $this->is_virtual())
{
return -1;
}
else
return 0;
}
// Creates an empty file
public function create()
{
if (! $this->exists() )
{
$fileDir = $this->getFileDir();
if (! is_null($fileDir) && $fileDir->exists())
{
$res = @touch($this->fullpath);
$this->exists = $res;
return $res;
}
}
else
Log::addWarn('Could not create/write file: ' . $this->fullpath);
return false;
}
public function append($message)
{
if (! $this->exists() )
$this->create();
if (! $this->is_writable() )
{
Log::addWarn('File append failed on ' . $this->getFullPath() . ' - not writable');
return false;
}
$handle = fopen($this->getFullPath(), 'a');
fwrite($handle, $message);
fclose($handle);
return true;
}
/** Copy a file to somewhere
*
* @param $destination String Full Path to new file.
*/
public function copy(FileModel $destination)
{
$sourcePath = $this->getFullPath();
$destinationPath = $destination->getFullPath();
Log::addDebug("Copy from $sourcePath to $destinationPath ");
if (! strlen($sourcePath) > 0 || ! strlen($destinationPath) > 0)
{
Log::addWarn('Attempted Copy on Empty Path', array($sourcePath, $destinationPath));
return false;
}
if (! $this->exists())
{
Log::addWarn('Tried to copy non-existing file - ' . $sourcePath);
return false;
}
$is_new = ($destination->exists()) ? false : true;
$status = @copy($sourcePath, $destinationPath);
if (! $status)
{
Log::addWarn('Could not copy file ' . $sourcePath . ' to' . $destinationPath);
}
else
{
$destination->resetStatus();
$destination->setFileInfo(); // refresh info.
}
//
do_action('shortpixel/filesystem/addfile', array($destinationPath, $destination, $this, $is_new));
return $status;
}
/** Move a file to somewhere
* This uses copy and delete functions and will fail if any of those fail.
* @param $destination String Full Path to new file.
*/
public function move(FileModel $destination)
{
$result = false;
if ($this->copy($destination))
{
$result = $this->delete();
if ($result == false)
{
Log::addError('Move can\'t remove file ' . $this->getFullPath());
}
$this->resetStatus();
$destination->resetStatus();
}
return $result;
}
/** Deletes current file
* This uses the WP function since it has a filter that might be useful
*/
public function delete()
{
if ($this->exists())
{
\wp_delete_file($this->fullpath); // delete file hook via wp_delete_file
}
else
{
Log::addWarn('Trying to remove non-existing file: ' . $this->getFullPath());
}
if (! file_exists($this->fullpath))
{
$this->resetStatus();
return true;
}
else {
$writable = ($this->is_writable()) ? 'true' : 'false';
Log::addWarn('File seems not removed - ' . $this->getFullPath() . ' (writable:' . $writable . ')');
return false;
}
}
public function getContents()
{
return file_get_contents($this->getFullPath());
}
public function getFullPath()
{
// filename here since fullpath is set unchecked in constructor, but might be a different take
if (is_null($this->filename))
{
$this->setFileInfo();
}
return $this->fullpath;
}
// Testing this. Principle is that when the plugin is absolutely sure this is a file, not something remote, not something non-existing, get the fullpath without any check.
// This function should *only* be used when processing mega amounts of files while not doing optimization or any processing.
// So far, testing use for file Filter */
public function getRawFullPath()
{
return $this->rawfullpath;
}
public function getFileName()
{
if (is_null($this->filename))
$this->setFileInfo();
return $this->filename;
}
public function getFileBase()
{
if (is_null($this->filebase))
$this->setFileInfo();
return $this->filebase;
}
public function getExtension()
{
if (is_null($this->extension))
$this->setFileInfo();
return $this->extension;
}
public function getMime()
{
if (is_null($this->mime))
$this->setFileInfo();
if ($this->exists() && ! $this->is_virtual() )
{
$this->mime = wp_get_image_mime($this->fullpath);
if (false === $this->mime)
{
$image_data = wp_check_filetype_and_ext($this->getFullPath(), $this->getFileName());
if (is_array($image_data) && isset($image_data['type']) && strlen($image_data['type']) > 0)
{
$this->mime = $image_data['type'];
}
}
}
else
$this->mime = false;
return $this->mime;
}
/* Util function to get location of backup Directory.
* @param Create - If true will try to create directory if it doesn't exist.
* @return Boolean | DirectModel Returns false if directory is not properly set, otherwhise with a new directoryModel
*/
protected function getBackupDirectory($create = false)
{
if (is_null($this->getFileDir()))
{
Log::addWarn('Could not establish FileDir ' . $this->fullpath);
return false;
}
$fs = \wpSPIO()->filesystem();
if (is_null($this->backupDirectory))
{
$directory = $fs->getBackupDirectory($this, $create);
if ($directory === false || ! $directory->exists()) // check if exists. FileModel should not attempt to create.
{
//Log::addWarn('Backup Directory not existing ' . $directory-);
return false;
}
elseif ($directory !== false)
{
$this->backupDirectory = $directory;
}
else
{
return false;
}
}
return $this->backupDirectory;
}
/* Internal function to check if path is a real path
* - Test for URL's based on http / https
* - Test if given path is absolute, from the filesystem root.
* @param $path String The file path
* @param String The Fixed filepath.
*/
protected function processPath($path)
{
$original_path = $path;
$fs = \wpSPIO()->filesystem();
if ($fs->pathIsUrl($path))
{
$path = $this->UrlToPath($path);
}
if ($path === false) // don't process further
return false;
$path = UtilHelper::spNormalizePath($path);
$abspath = $fs->getWPAbsPath();
// Prevent file operation below if trusted.
if (true === self::$TRUSTED_MODE)
{
return $path;
}
// Check if some openbasedir is active.
if (true === $this->fileIsRestricted($path))
{
$path = $this->relativeToFullPath($path);
}
if ( is_file($path) && ! is_dir($path) ) // if path and file exist, all should be okish.
{
return $path;
}
// If attempted file does not exist, but the file is in a dir that exists, that is good enough.
elseif ( ! is_dir($path) && is_dir(dirname($path)) )
{
return $path;
}
// If path is not in the abspath, it might be relative.
elseif (strpos($path, $abspath->getPath()) === false)
{
// if path does not contain basepath.
//$uploadDir = $fs->getWPUploadBase();
//$abspath = $fs->getWPAbsPath();
$path = $this->relativeToFullPath($path);
}
$path = apply_filters('shortpixel/filesystem/processFilePath', $path, $original_path);
/* This needs some check here on malformed path's, but can't be test for existing since that's not a requirement.
if (file_exists($path) === false) // failed to process path to something workable.
{
// Log::addInfo('Failed to process path', array($path));
$path = false;
} */
return $path;
}
protected function checkTrustedMode()
{
// When in trusted mode prevent filesystem checks as much as possible.
if (true === self::$TRUSTED_MODE)
{
// At this point file info might not be loaded, because it goes w/ construct -> processpath -> urlToPath etc on virtual files. And called via getFileInfo. Using any of the file info functions can trigger a loop.
if (is_null($this->extension))
{
$extension = pathinfo($this->fullpath, PATHINFO_EXTENSION);
}
else {
$extension = $this->getExtension();
}
$this->exists = true;
$this->is_writable = true;
$this->is_directory_writable = true;
$this->is_readable = true;
$this->is_file = true;
// Set mime to prevent lookup in IsImage
$this->mime = 'image/' . $extension;
if (is_null($this->filesize))
{
$this->filesize = 0;
}
}
}
/** Check if path is allowed within openbasedir restrictions. This is an attempt to limit notices in file funtions if so. Most likely the path will be relative in that case.
* @param String Path as String
*/
private function fileIsRestricted($path)
{
$basedir = ini_get('open_basedir');
if (false === $basedir || strlen($basedir) == 0)
{
return false;
}
$restricted = true;
$basedirs = preg_split('/:|;/i', $basedir);
foreach($basedirs as $basepath)
{
if (strpos($path, $basepath) !== false)
{
$restricted = false;
break;
}
}
return $restricted;
}
/** Resolve an URL to a local path
* This partially comes from WordPress functions attempting the same
* @param String $url The URL to resolve
* @return String/Boolean - False is this seems an external domain, otherwise resolved path.
*/
private function UrlToPath($url)
{
// If files is present, high chance that it's WPMU old style, which doesn't have in home_url the /files/ needed to properly replace and get the filepath . It would result in a /files/files path which is incorrect.
if (strpos($url, '/files/') !== false)
{
$uploadDir = wp_upload_dir();
$site_url = str_replace(array('http:', 'https:'), '', $uploadDir['baseurl']);
}
else {
$site_url = str_replace('http:', '', home_url('', 'http'));
}
$url = str_replace(array('http:', 'https:'), '', $url);
$fs = \wpSPIO()->filesystem();
// The site URL domain is included in the URL string
if (strpos($url, $site_url) !== false)
{
// try to replace URL for Path
$abspath = \wpSPIO()->filesystem()->getWPAbsPath();
$path = str_replace($site_url, rtrim($abspath->getPath(),'/'), $url);
if (! $fs->pathIsUrl($path)) // test again.
{
return $path;
}
}
$this->is_virtual = true;
/* This filter checks if some supplier will be able to handle the file when needed.
* Use translate filter to correct filepath when needed.
* Return could be true, or fileModel virtual constant
*/
$result = apply_filters('shortpixel/image/urltopath', false, $url);
if ($result === false)
{
$this->exists = false;
$this->is_readable = false;
$this->is_file = false;
}
else {
$this->exists = true;
$this->is_readable = true;
$this->is_file = true;
}
// If return is a stateless server, assume that it's writable and all that.
if ($result === self::$VIRTUAL_STATELESS)
{
$this->is_writable = true;
$this->is_directory_writable = true;
$this->virtual_status = self::$VIRTUAL_STATELESS;
}
elseif ($result === self::$VIRTUAL_REMOTE)
{
$this->virtual_status = self::$VIRTUAL_REMOTE;
}
return false; // seems URL from other server, use virtual mode.
}
/** Tries to find the full path for a perceived relative path.
*
* Relative path is detected on basis of WordPress ABSPATH. If this doesn't appear in the file path, it might be a relative path.
* Function checks for expections on this rule ( tmp path ) and returns modified - or not - path.
* @param $path The path for the file_exists
* @returns String The updated path, if that was possible.
*/
private function relativeToFullPath($path)
{
$originalPath = $path; // for safe-keeping
// A file with no path, can never be created to a fullpath.
if (strlen($path) == 0)
return $path;
// if the file plainly exists, it's usable /**
if (false === $this->fileIsRestricted($path) && file_exists($path))
{
return $path;
}
// Test if our 'relative' path is not a path to /tmp directory.
// This ini value might not exist.
$tempdirini = ini_get('upload_tmp_dir');
if ( (strlen($tempdirini) > 0) && strpos($path, $tempdirini) !== false)
return $path;
$tempdir = sys_get_temp_dir();
if ( (strlen($tempdir) > 0) && strpos($path, $tempdir) !== false)
return $path;
// Path contains upload basedir. This happens when upload dir is outside of usual WP.
$fs = \wpSPIO()->filesystem();
$uploadDir = $fs->getWPUploadBase();
$abspath = $fs->getWPAbsPath();
if (strpos($path, $uploadDir->getPath()) !== false) // If upload Dir is feature in path, consider it ok.
{
return $path;
}
elseif (file_exists($abspath->getPath() . $path)) // If upload dir is abspath plus return path. Exceptions.
{
return $abspath->getPath() . $path;
}
elseif(file_exists($uploadDir->getPath() . $path)) // This happens when upload_dir is not properly prepended in get_attachment_file due to WP errors
{
return $uploadDir->getPath() . $path;
}
// this is probably a bit of a sharp corner to take.
// if path starts with / remove it due to trailingslashing ABSPATH
$path = ltrim($path, '/');
$fullpath = $abspath->getPath() . $path;
// We can't test for file_exists here, since file_model allows non-existing files.
// Test if directory exists, perhaps. Otherwise we are in for a failure anyhow.
//if (is_dir(dirname($fullpath)))
return $fullpath;
//else
// return $originalPath;
}
public function getPermissions()
{
if (is_null($this->permissions))
$this->permissions = fileperms($this->getFullPath()) & 0777;
return $this->permissions;
}
// @tozo Lazy IMplementation / copy, should be brought in line w/ other attributes.
public function setPermissions($permissions)
{
@chmod($this->fullpath, $permissions);
}
/** Fix for multibyte pathnames and pathinfo which doesn't take into regard the locale.
* This snippet taken from PHPMailer.
*/
private function mb_pathinfo($path, $options = null)
{
$ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''];
$pathinfo = [];
if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^.\\\\/]+?)|))[\\\\/.]*$#m', $path, $pathinfo)) {
if (array_key_exists(1, $pathinfo)) {
$ret['dirname'] = $pathinfo[1];
}
if (array_key_exists(2, $pathinfo)) {
$ret['basename'] = $pathinfo[2];
}
if (array_key_exists(5, $pathinfo)) {
$ret['extension'] = $pathinfo[5];
}
if (array_key_exists(3, $pathinfo)) {
$ret['filename'] = $pathinfo[3];
}
}
switch ($options) {
case PATHINFO_DIRNAME:
case 'dirname':
return $ret['dirname'];
case PATHINFO_BASENAME:
case 'basename':
return $ret['basename'];
case PATHINFO_EXTENSION:
case 'extension':
return $ret['extension'];
case PATHINFO_FILENAME:
case 'filename':
return $ret['filename'];
default:
return $ret;
}
}
public function __debuginfo()
{
return [
'fullpath' => $this->fullpath,
'filename' => $this->filename,
'filebase' => $this->filebase,
'exists' => $this->exists,
'is_writable' => $this->is_writable,
'is_readable' => $this->is_readable,
'is_virtual' => $this->is_virtual,
];
}
} // FileModel Class