first
This commit is contained in:
@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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
|
Reference in New Issue
Block a user