praiadeseselle/wire/core/Pagefile.php
2022-03-08 15:55:41 +01:00

1466 lines
42 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php namespace ProcessWire;
/**
* ProcessWire Pagefile
*
* #pw-summary Represents a single file item attached to a page, typically via a File Fieldtype.
* #pw-summary-traversal For the most part youll want to traverse from the parent `Pagefiles` object than these methods.
* #pw-summary-manipulation Remember to follow up any manipulations with a `$pages->save()` call.
* #pw-summary-tags Be sure to see the `Pagefiles::getTag()` and `Pagesfiles::findTag()` methods, which enable you retrieve files by tag.
* #pw-use-constructor
* #pw-body =
* Pagefile objects are contained by a `Pagefiles` object.
* #pw-body
*
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
* https://processwire.com
*
* @property-read string $url URL to the file on the server.
* @property-read string $httpUrl URL to the file on the server including scheme and hostname.
* @property-read string $URL Same as $url property but with browser cache busting query string appended. #pw-group-other
* @property-read string $HTTPURL Same as the cache-busting uppercase “URL” property, but includes scheme and hostname. #pw-group-other
* @property-read string $filename full disk path to the file on the server.
* @property-read string $name Returns the filename without the path, same as the "basename" property.
* @property-read string $hash Get a unique hash (for the page) representing this Pagefile.
* @property-read array $tagsArray Get file tags as an array. #pw-group-tags @since 3.0.17
* @property-read array $fieldValues Custom field values. #pw-internal @since 3.0.142
* @property string $basename Returns the filename without the path.
* @property string $description Value of the files description field (string), if enabled. Note you can also set this property directly.
* @property string $tags Value of the files tags field (string), if enabled. #pw-group-tags
* @property string $ext Files extension (i.e. last 3 or so characters)
* @property-read int $filesize File size (number of bytes).
* @property int $modified Unix timestamp of when Pagefile (file, description or tags) was last modified. #pw-group-date-time
* @property-read string $modifiedStr Readable date/time string of when Pagefile was last modified. #pw-group-date-time
* @property-read int $mtime Unix timestamp of when file (only) was last modified. #pw-group-date-time
* @property-read string $mtimeStr Readable date/time string when file (only) was last modified. #pw-group-date-time
* @property int $created Unix timestamp of when file was created. #pw-group-date-time
* @property-read string $createdStr Readable date/time string of when Pagefile was created #pw-group-date-time
* @property string $filesizeStr File size as a formatted string, i.e. “123 Kb”.
* @property Pagefiles $pagefiles The Pagefiles WireArray that contains this file. #pw-group-other
* @property Page $page The Page object that this file is part of. #pw-group-other
* @property Field $field The Field object that this file is part of. #pw-group-other
* @property array $filedata
* @property int $created_users_id ID of user that added/uploaded the file or 0 if not known (3.0.154+). #pw-group-other
* @property int $modified_users_id ID of user that last modified the file or 0 if not known (3.0.154+). #pw-group-other
* @property User|NullPage $createdUser User that added/uploaded the file or NullPage if not known (3.0.154)+. #pw-group-other
* @property User|NullPage $modifiedUser User that last modified the file or NullPage if not known (3.0.154)+. #pw-group-other
*
* @method void install($filename)
* @method string httpUrl()
* @method string noCacheURL($http = false)
*
*/
class Pagefile extends WireData {
/**
* Timestamp 'created' used by pagefiles that are temporary, not yet published
*
*/
const createdTemp = 10;
/**
* Reference to the owning collection of Pagefiles
*
* @var Pagefiles
*
*/
protected $pagefiles;
/**
* @var PagefileExtra[]
*
*/
protected $extras = array();
/**
* Extra file data
*
* @var array
*
*/
protected $filedata = array();
/**
* Custom field values indexed by field name, loaded on request
*
* Values here have been run through wakeupValue and sanitizeValue already.
* Prior to that they are stored in $filedata (above).
*
* @var array
*
*/
protected $fieldValues = array();
/**
* Created user (populated only on rquest)
*
* @var User|null
*
*/
protected $_createdUser = null;
/**
* Modifed user (populated only on request)
*
* @var User|null
*
*/
protected $_modifiedUser = null;
/**
* Is this a brand new Pagefile rather than one loaded from DB?
*
* @var bool
*
*/
protected $_isNew = true;
/**
* Construct a new Pagefile
*
* ~~~~~
* // Construct a new Pagefile, assumes that $page->files is a FieldtypeFile Field
* $pagefile = new Pagefile($page->files, '/path/to/file.pdf');
* ~~~~~
*
* @param Pagefiles $pagefiles The Pagefiles WireArray that will contain this file.
* @param string $filename Full path and filename to this Pagefile.
*
*/
public function __construct(Pagefiles $pagefiles, $filename) {
$this->pagefiles = $pagefiles;
if(strlen($filename)) $this->setFilename($filename);
$this->set('description', '');
$this->set('tags', '');
$this->set('formatted', false); // has an output formatter been run on this Pagefile?
$this->set('modified', 0);
$this->set('created', 0);
$this->set('filesize', 0);
$this->set('created_users_id', 0);
$this->set('modified_users_id', 0);
}
/**
* Clone Pagefile
*
* #pw-internal
*
*/
public function __clone() {
$this->extras = array();
$this->set('filesize', 0);
$this->set('created_users_id', 0);
$this->set('modified_users_id', 0);
$this->_createdUser = null;
$this->_modifiedUser = null;
$this->isNew(true);
parent::__clone();
}
/**
* Set the filename associated with this Pagefile
*
* No need to call this as it's already called from the constructor.
* This exists so that Pagefile/Pageimage descendents can create cloned variations, if applicable.
*
* #pw-internal
*
* @param string $filename
*
*/
public function setFilename($filename) {
$basename = basename($filename);
if(DIRECTORY_SEPARATOR != '/') {
// To correct issue with XAMPP in Windows
$filename = str_replace('\\' . $basename, '/' . $basename, $filename);
}
if($basename != $filename && strpos($filename, $this->pagefiles->path()) !== 0) {
$this->install($filename);
} else {
$this->set('basename', $basename);
}
}
/**
* Install this Pagefile
*
* Implies copying the file to the correct location (if not already there), and populating its name.
* The given $filename may be local (path) or external (URL).
*
* #pw-hooker
*
* @param string $filename Full path and filename of file to install, or http/https URL to pull file from.
* @throws WireException
*
*/
protected function ___install($filename) {
$basename = $filename;
if(strpos($basename, '?') !== false) {
list($basename, $queryString) = explode('?', $basename);
if($queryString) {} // do not use in basename
}
if(empty($basename)) throw new WireException("Empty filename");
$basename = $this->pagefiles->cleanBasename($basename, true, false, true);
$pathInfo = pathinfo($basename);
$basename = basename($basename, ".$pathInfo[extension]");
$basenameNoExt = $basename;
$basename .= ".$pathInfo[extension]";
// ensure filename is unique
$cnt = 0;
while(file_exists($this->pagefiles->path() . $basename)) {
$cnt++;
$basename = "$basenameNoExt-$cnt.$pathInfo[extension]";
}
$destination = $this->pagefiles->path() . $basename;
if(strpos($filename, '://') === false) {
if(!is_readable($filename)) throw new WireException("Unable to read: $filename");
if(!copy($filename, $destination)) throw new WireException("Unable to copy: $filename => $destination");
} else {
$http = $this->wire(new WireHttp());
// note: download() method throws excepton on failure
$http->download($filename, $destination);
// download was successful
}
$this->wire('files')->chmod($destination);
$this->changed('file');
$this->isNew(true);
parent::set('basename', $basename);
}
/**
* Sets a value in this Pagefile
*
* Externally, this would be used to set the files basename or description
*
* #pw-internal
*
* @param string $key
* @param mixed $value
* @return Pagefile|WireData
*
*/
public function set($key, $value) {
if($key === 'basename') {
$value = $this->pagefiles->cleanBasename($value, false);
} else if($key === 'description') {
return $this->setDescription($value);
} else if($key === 'modified') {
$value = ctype_digit("$value") ? (int) $value : strtotime($value);
} else if($key === 'created') {
$value = ctype_digit("$value") ? (int) $value : strtotime($value);
} else if($key === 'created_users_id' || $key === 'createdUser') {
$this->setUser($value, 'created');
return $this;
} else if($key === 'modified_users_id' || $key === 'modifiedUser') {
$this->setUser($value, 'modified');
return $this;
} else if($key === 'tags') {
$this->tags($value);
return $this;
} else if($key === 'filedata') {
if(is_array($value)) $this->filedata($value);
return $this;
} else if($key === 'filesize') {
$value = (int) $value;
if(empty($this->data['filesize'])) {
$this->data['filesize'] = $value;
return $this;
}
}
if(strpos($key, 'description') === 0 && preg_match('/^description(\d+)$/', $value, $matches)) {
// check if a language description is being set manually by description123 where 123 is language ID
$languages = $this->wire('languages');
if($languages) {
$language = $languages->get((int) $matches[1]);
if($language && $language->id) return $this->setDescription($value, $language);
}
} else if($this->setFieldValue($key, $value)) {
return $this;
}
return parent::set($key, $value);
}
/**
* Set user that created or modified this file
*
* #pw-internal
*
* @param User|int|string|true $user Specify user object, name, ID, or boolean true for current user
* @param $type 'created' or 'modified'
* @since 3.0.154
*
*/
protected function setUser($user, $type) {
$id = 0;
if($user === true) $user = $this->wire('user');
if(is_object($user)) {
if($user instanceof NullPage) {
$id = 0;
} else if($user instanceof User) {
$id = $user->isGuest() ? 0 : $user->id;
}
} else if(is_int($user)) {
$id = $user;
} else if(ctype_digit($user)) {
$id = (int) $user;
} else if(is_string($user)) {
$name = $this->wire('sanitizer')->pageName($user);
$user = $name ? $this->wire('users')->get("name=$name") : null;
$id = $user && $user->id ? $user->id : 0;
}
if($id < 0) $id = 0;
if(strpos($type, 'created') === 0) {
$this->_createdUser = ($id && $user instanceof User ? $user : null);
parent::set('created_users_id', $id);
} else if(strpos($type, 'modified') === 0) {
$this->_modifiedUser = ($id && $user instanceof User ? $user : null);
parent::set('modified_users_id', $id);
}
}
/**
* Get created/modified user
*
* #pw-internal
*
* @param string $type One of 'created' or 'modified'
* @return User|NullPage
* @since 3.0.154
*
*/
protected function getUser($type) {
$type = strpos($type, 'created') === 0 ? 'created' : 'modified';
$key = $type === 'created' ? '_createdUser' : '_modifiedUser';
if(!$this->$key) {
$id = (int) parent::get($type . '_users_id');
$this->$key = $id ? $this->wire('users')->get($id) : new NullPage();
}
return $this->$key;
}
/**
* Get or set filedata
*
* Filedata is any additional data that you want to store with the files database record.
*
* - To get a value, specify just the $key argument. Null is returned if request value is not present.
* - To get all values, omit all arguments. An associative array will be returned.
* - To set a value, specify the $key and the $value to set.
* - To set all values at once, specify an associative array for the $key argument.
* - To unset, specify boolean false (or null) for $key, and the name of the property to unset as $value.
* - To unset, you can also get all values, unset it from the retuned array, and set the array back.
*
* #pw-group-manipulation
*
* @param string|array|false|null $key Specify array to set all file data, or key (string) to set or get a property,
* Or specify boolean false to remove key specified by $value argument.
* @param null|string|array|int|float $value Specify a value to set for given property
* @return Pagefile|Pageimage|array|string|int|float|bool|null
*
*/
public function filedata($key = '', $value = null) {
$filedata = $this->filedata;
$changed = false;
if($key === false || $key === null) {
// unset property named in $value
if(!empty($value) && isset($filedata[$value])) {
unset($this->filedata[$value]);
$changed = true;
}
} else if(empty($key)) {
// return all
return $filedata;
} else if(is_array($key)) {
// set all
if($key != $filedata) {
$this->filedata = $key;
$changed = true;
}
} else if($value === null) {
// return value for key
return isset($this->filedata[$key]) ? $this->filedata[$key] : null;
} else {
// set value for key
if(!isset($filedata[$key]) || $filedata[$key] != $value) {
$this->filedata[$key] = $value;
$changed = true;
}
}
if($changed) {
$this->trackChange('filedata', $filedata, $this->filedata);
if($this->page && $this->field) $this->page->trackChange($this->field->name);
}
return $this;
}
/**
* Set a description, optionally parsing JSON language-specific descriptions to separate properties
*
* @param string|array $value
* @param Page|Language Langage to set it for. Omit to determine automatically.
* @return $this
*
*/
protected function setDescription($value, Page $language = null) {
/** @var Languages $languages */
$languages = $this->wire('languages');
/** @var Language|null $language */
$field = $this->field;
$noLang = $field && $field->get('noLang'); // noLang setting to disable multi-language from InputfieldFile
if(!is_null($language) && $language->id) {
$name = "description";
if(!$language->isDefault() && !$noLang) {
$name .= $language->id;
}
parent::set($name, $value);
if($name != 'description' && $this->isChanged($name)) $this->trackChange('description');
return $this;
}
if(is_array($value)) {
$values = $value;
} else {
// check if it contains JSON?
$first = substr($value, 0, 1);
$last = substr($value, -1);
if(($first == '{' && $last == '}') || ($first == '[' && $last == ']')) {
$values = json_decode($value, true);
} else {
$values = array();
}
}
$numChanges = 0;
if($values && count($values)) {
$n = 0;
foreach($values as $id => $v) {
// first item is always default language. this ensures description will still
// work even if language support is later uninstalled.
$name = 'description';
if($noLang && $n > 0) break;
$n++;
if(ctype_digit("$id")) {
$id = (int) $id;
if(!$id) $id = '';
$name = $n > 0 ? "description$id" : "description";
} else if($id === 'default') {
$name = 'description';
} else if($languages) {
$language = $languages->get($id); // i.e. "default" or "es"
if(!$language->id) continue;
$name = $language->isDefault() ? "description" : "description$language->id";
}
parent::set($name, $v);
if($this->isChanged($name)) $numChanges++;
}
} else {
// no JSON values so assume regular language description
$languages = $this->wire('languages');
$language = $languages ? $this->wire('user')->language : null;
if($languages && $language && !$noLang && !$language->isDefault()) {
$name = "description$language->id";
} else {
$name = "description";
}
parent::set($name, $value);
if($this->isChanged($name)) $numChanges++;
}
if($numChanges && !$this->isChanged('description')) $this->trackChange('description');
return $this;
}
/**
* Get or set the files description (with multi-language support).
*
* When not in a multi-language environment, you can still use this method but we recommend using the simpler method of just
* getting/seting the `Pagefile::$description` property directly instead.
*
* ~~~~~
* // Get a Pagefile to work with
* $pagefile = $page->files->first();
*
* // Setting description
* $pagefile->description('en', 'Setting English description');
* $pagefile->description('de', 'Setting German description');
*
* // Getting description for current language (whatever it happens to be)
* echo $pagefile->description();
*
* // Getting description for language "de"
* echo $pagefile->description('de');
* ~~~~~
*
* #pw-group-common
* #pw-group-manipulation
*
* @param null|bool|Language|array
* - To GET in current user language: Omit arguments or specify null.
* - To GET in another language: Specify a Language name, id or object.
* - To GET in all languages as a JSON string: Specify boolean true (if LanguageSupport not installed, regular string returned).
* - To GET in all languages as an array indexed by language name: Specify boolean true for both arguments.
* - To SET for a language: Specify a language name, id or object, plus the $value as the 2nd argument.
* - To SET in all languages as a JSON string: Specify boolean true, plus the JSON string $value as the 2nd argument (internal use only).
* - To SET in all languages as an array: Specify the array here, indexed by language ID or name, and omit 2nd argument.
* @param null|string $value Specify only when you are setting (single language) rather than getting a value.
* @return string|array
*
*/
public function description($language = null, $value = null) {
if($language === true && $value === true) {
// return all in array indexed by language name
/** @var Languages $languages */
$languages = $this->wire('languages');
if(!$languages) return array('default' => parent::get('description'));
$value = array();
foreach($languages as $language) {
$value[$language->name] = (string) parent::get("description" . ($language->isDefault() ? '' : $language->id));
}
return $value;
}
if(!is_null($value)) {
// set description mode
if($language === true) {
// set all language descriptions
$this->setDescription($value);
} else {
// set specific language description
$this->setDescription($value, $language);
}
return $value;
}
if(is_array($language)) {
// set all from array, then return description in current language
$this->setDescription($language);
$language = null;
$value = null;
}
if((is_string($language) || is_int($language)) && $this->wire('languages')) {
// convert named or ID'd languages to Language object
$language = $this->wire('languages')->get($language);
}
if(is_null($language)) {
// return description for current user language, or inherit from default if not available
$user = $this->wire('user');
$value = null;
if($user->language && $user->language->id) $value = parent::get("description{$user->language}");
if(empty($value)) {
// inherit default language value
$value = parent::get("description");
}
} else if($language === true) {
// return JSON string of all languages if applicable
$languages = $this->wire('languages');
if($languages && $languages->count() > 1) {
$values = array(0 => parent::get("description"));
foreach($languages as $lang) {
if($lang->isDefault()) continue;
$v = parent::get("description$lang");
if(empty($v)) continue;
$values[$lang->id] = $v;
}
$flags = defined("JSON_UNESCAPED_UNICODE") ? JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES : 0; // more fulltext friendly
$value = json_encode($values, $flags);
} else {
// no languages present so just return string with description
$value = parent::get("description");
}
} else if(is_object($language) && $language->id) {
// return description for specific language or blank if not available
if($language->isDefault()) $value = parent::get("description");
else $value = parent::get("description$language");
}
// we only return strings, so return blank rather than null
if(is_null($value)) $value = '';
return $value;
}
/**
* Get a value from this Pagefile
*
* #pw-internal
*
* @param string $key
* @return mixed Returns null if value does not exist
*
*/
public function get($key) {
$value = null;
if($key == 'name') $key = 'basename';
if($key == 'pathname') $key = 'filename';
switch($key) {
case 'url':
case 'httpUrl':
case 'filename':
case 'description':
case 'tags':
case 'ext':
case 'hash':
case 'filesize':
case 'filesizeStr':
// 'basename' property intentionally excluded
$value = $this->$key();
break;
case 'tagsArray':
$value = $this->tags(true);
break;
case 'URL':
// nocache url
$value = $this->noCacheURL();
break;
case 'HTTPURL':
$value = $this->noCacheURL(true);
break;
case 'pagefiles':
$value = $this->pagefiles;
break;
case 'page':
$value = $this->pagefiles->getPage();
break;
case 'field':
$value = $this->pagefiles->getField();
break;
case 'modified':
case 'created':
$value = parent::get($key);
if(empty($value)) {
$value = $this->filemtime();
parent::set($key, $value);
}
break;
case 'modifiedStr':
case 'createdStr':
$value = parent::get(str_replace('Str', '', $key));
$value = wireDate($this->wire('config')->dateFormat, $value);
break;
case 'created_users_id':
case 'modified_users_id':
$value = (int) parent::get($key);
break;
case 'createdUser':
case 'modifiedUser':
$value = $this->getUser($key);
break;
case 'fileData':
case 'filedata':
$value = $this->filedata();
break;
case 'mtime':
case 'filemtime':
$value = $this->filemtime();
break;
case 'mtimeStr':
case 'filemtimeStr':
$value = wireDate($this->wire('config')->dateFormat, $this->filemtime());
break;
case 'fieldValues':
return $this->fieldValues;
break;
default:
$value = $this->getFieldValue($key);
}
if(is_null($value)) return parent::get($key);
return $value;
}
/**
* Get a custom field value
*
* #pw-internal Most non-core cases should just use get() or direct access rather than this method
*
* @param string $name
* @param bool|null Get as formatted value? true=yes, false=no, null=use page output formatting setting (default=null)
* @return mixed|null Returns value or null if not present
* @since 3.0.142
*
*/
public function getFieldValue($name, $formatted = null) {
$field = $this->wire('fields')->get($name);
if(!$field) return null;
$template = $this->pagefiles->getFieldsTemplate();
if(!$template) return null;
$fieldgroup = $template->fieldgroup;
if(!$fieldgroup->hasField($field)) return null;
$field = $fieldgroup->getFieldContext($field); // get in context
$fieldtype = $field->type; /** @var Fieldtype $fieldtype */
$fileField = $this->pagefiles->getField(); /** @var Field $fileField */
$fileFieldtype = $fileField->type; /** @var FieldtypeFile|FieldtypeImage $fileFieldtype */
$page = $fileFieldtype->getFieldsPage($fileField);
if(array_key_exists($name, $this->fieldValues)) {
$value = $this->fieldValues[$name];
} else {
$idKey = "_$field->id";
$value = $this->filedata($idKey);
if($value !== null) {
$value = $fieldtype->wakeupValue($page, $field, $value);
$value = $fieldtype->sanitizeValue($page, $field, $value);
}
$this->fieldValues[$name] = $value;
unset($this->filedata[$idKey]); // avoid storing double copies
}
if($value === null) {
$value = $fieldtype->getBlankValue($page, $field);
$value = $fieldtype->sanitizeValue($page, $field, $value);
$this->fieldValues[$name] = $value;
}
if($formatted === null) $formatted = $this->page->of();
if($formatted) $value = $fieldtype->formatValue($page, $field, $value);
return $value;
}
/**
* Set a custom field value
*
* #pw-internal Most non-core cases should use set() instead
*
* @param string $name
* @param mixed $value
* @param bool|null $changed Specify true to force track change, false to force no change, or null to auto-detect (default=null)
* @return bool Returns true if value set, or false if not (like if theres no template defined for the purpose)
* @since 3.0.142
*
*
*/
public function setFieldValue($name, $value, $changed = null) {
$template = $this->pagefiles->getFieldsTemplate();
if(!$template) return false;
$fieldgroup = $template->fieldgroup;
if(!$fieldgroup) return false;
$field = $fieldgroup->getFieldContext($name);
if(!$field) return false;
$page = $this->pagefiles->getFieldsPage();
/** @var Fieldtype $fieldtype */
$fieldtype = $field->type;
$value = $fieldtype->sanitizeValue($page, $field, $value);
if($changed === null && $this->page->trackChanges()) {
// detect if a change has taken place
$oldValue = $this->getFieldValue($field->name, false);
if(is_object($oldValue) && $oldValue instanceof Wire && $oldValue === $value) {
// $oldValue and new $value are the same object instance, so ask it if anything has changed
$changed = $oldValue->isChanged();
if($changed) $this->trackChange($field->name);
} else if($oldValue != $value) {
// $oldValue and new $value differ, record change
$this->trackChange($field->name, $oldValue, $value);
}
} else if($changed === true) {
$this->trackChange($field->name);
}
$this->fieldValues[$field->name] = $value;
return true;
}
/**
* Hookable no-cache URL
*
* #pw-internal
*
* @param bool $http Include scheme and hostname?
* @return string
*
*/
public function ___noCacheURL($http = false) {
return ($http ? $this->httpUrl() : $this->url()) . '?nc=' . $this->filemtime();
}
/**
* Return the next sibling Pagefile in the parent Pagefiles, or NULL if at the end.
*
* #pw-group-traversal
*
* @return Pagefile|Wire|null
*
*/
public function getNext() {
return $this->pagefiles->getNext($this);
}
/**
* Return the previous sibling Pagefile in the parent Pagefiles, or NULL if at the beginning.
*
* #pw-group-traversal
*
* @return Pagefile|Wire|null
*
*/
public function getPrev() {
return $this->pagefiles->getPrev($this);
}
/**
* Return the web accessible URL to this Pagefile.
*
* ~~~~~
* // Example of using the url method/property
* foreach($page->files as $file) {
* echo "<li><a href='$file->url'>$file->description</a></li>";
* }
* ~~~~~
*
* #pw-hooks
* #pw-common
*
* @return string
* @see Pagefile:httpUrl()
*
*/
public function url() {
return $this->wire('hooks')->isHooked('Pagefile::url()') ? $this->__call('url', array()) : $this->___url();
}
/**
* Hookable version of url() method
*
* @return string
*
*/
protected function ___url() {
return $this->pagefiles->url . $this->basename;
}
/**
* Return the web accessible URL (with scheme and hostname) to this Pagefile.
*
* @return string
* @see Pagefile::url()
*
*/
public function ___httpUrl() {
$page = $this->pagefiles->getPage();
$url = substr($page->httpUrl(), 0, -1 * strlen($page->url()));
return $url . $this->url();
}
/**
* Returns the full disk path name filename to the Pagefile.
*
* #pw-hooks
* #pw-common
*
* @return string
*
*/
public function filename() {
return $this->wire('hooks')->isHooked('Pagefile::filename()') ? $this->__call('filename', array()) : $this->___filename();
}
/**
* Hookable version of filename() method
*
*/
protected function ___filename() {
return $this->pagefiles->path . $this->basename;
}
/**
* Returns the basename of this Pagefile (name and extension, without disk path).
*
* @param bool $ext Specify false to exclude the extension (default=true)
* @return string
*
*/
public function basename($ext = true) {
$basename = parent::get('basename');
if(!$ext) $basename = basename($basename, "." . $this->ext());
return $basename;
}
/**
* Get or set the "tags" property, when in use.
*
* ~~~~~
* $file = $page->files->first();
* $tags = $file->tags(); // Get tags string
* $tags = $file->tags(true); // Get tags array
* $file->tags("foo bar baz"); // Set tags to be these 3 tags
* $tags->tags(["foo", "bar", "baz"]); // Same as above, using array
* ~~~~~
*
* #pw-group-tags
* #pw-group-manipulation
*
* @param bool|string|array $value Specify one of the following:
* - Omit to simply return the tags as a string.
* - Boolean true if you want to return tags as an array (rather than string).
* - Boolean false to return tags as an array, with lowercase enforced.
* - String or array if you are setting the tags.
* @return string|array Returns the current tags as a string or an array.
* When an array is returned, it is an associative array where the key and value are both the tag (keys are always lowercase).
* @see Pagefile::addTag(), Pagefile::hasTag(), Pagefile::removeTag()
*
*/
public function tags($value = null) {
if(is_bool($value)) {
// return array of tags
$tags = parent::get('tags');
$tags = str_replace(array(',', '|'), ' ', $tags);
$_tags = explode(' ', $tags);
$tags = array();
foreach($_tags as $key => $tag) {
$tag = trim($tag);
if($value === false) $tag = strtolower($tag); // force lowercase
if(!strlen($tag)) continue;
$tags[strtolower($tag)] = $tag;
}
} else if($value !== null) {
// set tags
if(is_array($value)) $value = implode(' ', $value); // convert to string
$value = $this->wire('sanitizer')->text($value);
if(strpos($value, "\t") !== false) $value = str_replace("\t", " ", $value);
// collapse extra whitespace
while(strpos($value, " ") !== false) $value = str_replace(" ", " ", $value);
parent::set('tags', $value);
$tags = $value;
} else {
// just get tags string
$tags = parent::get('tags');
}
return $tags;
}
/**
* Does this file have the given tag(s)?
*
* ~~~~~
* $file = $page->files->first();
*
* if($file->hasTag('foobar')) {
* // file has the "foobar" tag
* }
*
* if($file->hasTag("foo|baz")) {
* // file has either the foo OR baz tag
* }
*
* if($file->hasTag("foo,baz")) {
* // file has both the foo AND baz tags (since 3.0.17)
* }
* ~~~~~
*
* #pw-changelog 3.0.17 Added support for AND mode, where multiple tags can be specified and all must be present to return true.
* #pw-changelog 3.0.17 OR mode now returns found tag rather than boolean true.
* #pw-group-tags
*
* @param string $tag Specify one of the following:
* - Single tag without whitespace.
* - Multiple tags separated by a "|" to determine if Pagefile has at least one of the tags.
* - Multiple tags separated by a comma to determine if Pagefile has all of the tags. (since 3.0.17)
* @return bool|string True if it has the given tag(s), false if not.
* - If multiple tags were specified separated by a "|", then if at least one was present, this method returns the found tag.
* - If multiple tags were specified separated by a space or comma, and all tags are present, it returns true. (since 3.0.17)
* @see Pagefile::tags(), Pagefile::addTag(), Pagefile::removeTag()
*
*/
public function hasTag($tag) {
$tags = $this->tags(false); // all tags in array, lowercase
if(empty($tags)) return false;
$modeAND = null;
$tag = trim(strtolower($tag));
if(strpos($tag, '|') !== false) {
$findTags = explode('|', $tag);
$modeAND = false;
} else if(strpos($tag, ',') !== false) {
$findTags = explode(',', $tag);
$modeAND = true;
} else {
$findTags = array($tag);
}
$numTags = 0;
$numFound = 0;
$tagFound = '';
foreach($findTags as $tag) {
$tag = trim($tag);
if(!strlen($tag)) continue;
$tag = str_replace(' ', '_', $tag);
$numTags++;
if(isset($tags[$tag])) {
$numFound++;
if($modeAND === false) {
$tagFound = $tag;
break;
}
}
}
if($modeAND === false) {
// OR mode: must have at least one of given tags, and we return the found tag
return $numFound > 0 ? $tagFound : false;
} else if($modeAND === true) {
// AND mode: must have all of the given tags
return $numFound == $numTags;
}
// single tag
return $numFound > 0;
}
/**
* Add the given tag to this files tags (if not already present)
*
* ~~~~~
* $file = $page->files->first();
* $file->addTag('foo'); // add single tag
* $file->addTag('foo,bar,baz'); // add multiple tags
* $file->addTag(['foo', 'bar', 'baz']); // same as above, using array
* ~~~~~
*
* #pw-group-tags
* #pw-group-manipulation
*
* @param string|array $tag Tag to add, or array of tags to add, or CSV string of tags to add.
* @return $this
* @since 3.0.17
* @see Pagefile::tags(), Pagefile::hasTag(), Pagefile::removeTag()
*
*/
public function addTag($tag) {
if(is_array($tag)) {
$addTags = $tag;
} else if(strpos($tag, ',') !== false) {
$addTags = explode(',', $tag);
} else {
$addTags = array($tag);
}
$tags = $this->tags(true);
$numAdded = 0;
foreach($addTags as $tag) {
if($this->hasTag($tag)) continue;
$tag = $this->wire('sanitizer')->text(trim($tag));
$tag = str_replace(' ', '_', $tag);
$tags[strtolower($tag)] = $tag;
$numAdded++;
}
if($numAdded) $this->tags($tags);
return $this;
}
/**
* Remove the given tag from this files tags (if present)
*
* ~~~~~
* $file = $page->files->first();
* $file->removeTag('foo'); // remove single tag
* $file->removeTag('foo,bar,baz'); // remove multiple tags
* $file->removeTag(['foo', 'bar', 'baz']); // same as above, using array
* ~~~~~
*
* #pw-group-tags
* #pw-group-manipulation
*
* @param string $tag Tag to remove, or array of tags to remove, or CSV string of tags to remove.
* @return $this
* @since 3.0.17
* @see Pagefile::tags(), Pagefile::hasTag(), Pagefile::addTag()
*
*/
public function removeTag($tag) {
$tags = $this->tags(true);
if(!count($tags)) return $this; // no tags to remove
if(is_array($tag)) {
$removeTags = $tag;
} else if(strpos($tag, ',') !== false) {
$removeTags = explode(',', $tag);
} else {
$removeTags = array($tag);
}
$numRemoved = 0;
foreach($removeTags as $tag) {
$tag = strtolower(trim($tag));
$tag = str_replace(' ', '_', $tag);
if(!isset($tags[$tag])) continue;
unset($tags[strtolower($tag)]);
$numRemoved++;
}
if($numRemoved) $this->tags($tags);
return $this;
}
/**
* Has the output already been formatted?
*
* #pw-internal
*
*/
public function formatted() {
return parent::get('formatted') ? true : false;
}
/**
* Get last modified time of file
*
* @param bool $reset
* @return int Unix timestamp
* @since 3.0.154
*
*/
public function filemtime($reset = false) {
if($reset) {} // @todo
return (int) @filemtime($this->filename());
}
/**
* Returns the filesize in number of bytes.
*
* @param bool $reset
* @return int
*
*/
public function filesize($reset = false) {
if($reset) {} // @todo
$filesize = (int) @filesize($this->filename());
return $filesize;
}
/**
* Returns the filesize in a formatted, output-ready string (i.e. "123 kB")
*
* @return string
*
*/
public function filesizeStr() {
return wireBytesStr($this->filesize());
}
/**
* Returns the files extension - "pdf", "jpg", etc.
*
* @return string
*
*/
public function ext() {
return substr($this->basename(), strrpos($this->basename(), '.')+1);
}
/**
* When dereferenced as a string, a Pagefile returns its basename
*
* @return string
*
*/
public function __toString() {
return (string) $this->basename;
}
/**
* Return a unique MD5 hash representing this Pagefile.
*
* This hash can be counted on to be unique among all files on a given page, regardless of field.
*
* @return string
*
*/
public function hash() {
$hash = parent::get('hash');
if($hash) return $hash;
$this->set('hash', md5($this->basename()));
return parent::get('hash');
}
/**
* Delete the physical file on disk, associated with this Pagefile
*
* #pw-internal Public API should use removal methods from the parent Pagefiles.
*
* @return bool True on success, false on fail
*
*/
public function unlink() {
/** @var WireFileTools $files */
if(!strlen($this->basename) || !is_file($this->filename)) return true;
$files = $this->wire('files');
foreach($this->extras() as $extra) {
$extra->unlink();
}
return $files->unlink($this->filename, true);
}
/**
* Rename this file
*
* Remember to follow this up with a `$page->save()` for the page that the file lives on.
*
* #pw-group-manipulation
*
* @param string $basename New name to use. Must be just the file basename (no path).
* @return string|bool Returns new name (basename) on success, or boolean false if rename failed.
*
*/
public function rename($basename) {
foreach($this->extras() as $extra) {
$extra->filename(); // init
}
$basename = $this->pagefiles->cleanBasename($basename, true);
if($this->wire('files')->rename($this->filename, $this->pagefiles->path . $basename, true)) {
$this->set('basename', $basename);
$basename = $this->basename();
foreach($this->extras() as $extra) {
$extra->rename();
}
return $basename;
}
return false;
}
/**
* Copy this file to the new specified path
*
* #pw-internal
*
* @param string $path Path (not including basename)
* @return bool result of copy() function
*
*/
public function copyToPath($path) {
/** @var WireFileTools $files */
$files = $this->wire('files');
$result = $files->copy($this->filename(), $path);
foreach($this->extras() as $extra) {
if(!$extra->exists()) continue;
$files->copy($extra->filename, $path);
}
return $result;
}
/**
* Hook called a property changes (from Wire)
*
* Alert the $pagefiles of the change
*
* #pw-internal
*
* @param string $what
* @param mixed $old
* @param mixed $new
*
*/
public function ___changed($what, $old = null, $new = null) {
if(in_array($what, array('description', 'tags', 'file', 'filedata')) || array_key_exists($what, $this->fieldValues)) {
$this->setUser(true, 'modified');
$this->set('modified', time());
$this->pagefiles->trackChange('item');
}
parent::___changed($what, $old, $new);
}
/**
* Set the parent array container
*
* #pw-internal
*
* @param Pagefiles $pagefiles
* @return $this
*
*/
public function setPagefilesParent(Pagefiles $pagefiles) {
$this->pagefiles = $pagefiles;
return $this;
}
/**
* Get or set the temporary status of the Pagefile
*
* Returns true if this Pagefile is temporary, not yet published. Or use this to set the temp status.
*
* #pw-internal
*
* @param bool $set Optionally set the temp status to true or false
* @return bool
*
*/
public function isTemp($set = null) {
return $this->pagefiles->isTemp($this, $set);
}
/**
* Get or set “new” status of the Pagefile
*
* This is true with a Pagefile that was created during this request and not loaded from DB.
*
* @param bool|null $set
* @return bool
*
*/
public function isNew($set = null) {
if(is_bool($set)) $this->_isNew = $set;
return $this->_isNew;
}
/**
* Get all extras, add an extra, or get an extra
*
* #pw-internal
*
* @param string $name
* @param PagefileExtra $value
* @return PagefileExtra[]|PagefileExtra|null
* @since 3.0.132
*
*/
public function extras($name = null, PagefileExtra $value = null) {
if($name === null) return $this->extras;
if($value !== null && $value instanceof PagefileExtra) {
$this->extras[$name] = $value;
}
return isset($this->extras[$name]) ? $this->extras[$name] : null;
}
/**
* Save this Pagefile independently of the Page it lives on
*
* @return bool
* @throws WireException
* @since 3.0.154
*
*/
public function save() {
/** @var FieldtypeFile $fieldtype */
$fieldtype = $this->field->type;
return $fieldtype->saveFile($this->page, $this->field, $this);
}
/**
* Replace file with another
*
* Should be followed up with a save() to ensure related properties are also committed to DB.
*
* #pw-internal
*
* @param string $filename File to replace current one with
* @param bool $move Move given $filename rather than copy? (default=true)
* @return bool
* @throws WireException
* @since 3.0.154
*
*/
public function replaceFile($filename, $move = true) {
/** @var WireFileTools $files */
$files = $this->wire('files');
if(!is_file($filename) || !is_readable($filename)) return false;
if($move && !is_writable($filename)) $move = false;
$srcFile = $filename;
$dstFile = $this->filename();
$tmpFile = dirname($dstFile) . '/.' . basename($dstFile) . '.tmp';
if(file_exists($tmpFile)) $files->unlink($tmpFile);
$files->rename($dstFile, $tmpFile);
if($move) {
$result = $files->rename($srcFile, $dstFile);
} else {
$result = $files->copy($srcFile, $dstFile);
}
if(!$result) {
$files->rename($tmpFile, $dstFile);
return false;
}
$files->unlink($tmpFile);
$this->filesize(true);
$this->filemtime(true);
return true;
}
/**
* Ensures that isset() and empty() work for dynamic class properties
*
* @param string $key
* @return bool
*
*/
public function __isset($key) {
if(parent::__isset($key)) return true;
return $this->get($key) !== null;
}
/**
* Debug info
*
* @return array
*
*/
public function __debugInfo() {
$filedata = $this->filedata();
if(empty($filedata)) $filedata = null;
$info = array(
'url' => $this->url(),
'filename' => $this->filename(),
'filesize' => $this->filesize(),
'description' => $this->description,
'tags' => $this->tags,
'created' => $this->createdStr,
'modified' => $this->modifiedStr,
'created_users_id' => $this->created_users_id,
'modified_users_id' => $this->modified_users_id,
'filemtime' => $this->mtimeStr,
'filedata' => $filedata,
);
if(empty($info['filedata'])) unset($info['filedata']);
return $info;
}
}