<?php
namespace MatthiasWeb\RealMediaLibrary\folder;
use MatthiasWeb\RealMediaLibrary\attachment;
use MatthiasWeb\RealMediaLibrary\general;
use MatthiasWeb\RealMediaLibrary\api;
use MatthiasWeb\RealMediaLibrary\order;
defined( 'ABSPATH' ) or die( 'No script kiddies please!' );
/**
* Abstract class for a creatable folder item. It handles all general
* actions for a folder item. If you want to add an new folder type, have a
* look at the api function wp_rml_register_creatable();
*
* A new folder type MUST have the implementation with class FOLDERTYPE
* extends order\Sortable because every folder can also be sortable!
*/
abstract class Creatable extends BaseFolder {
/**
* C'tor with the main properties.
*
* The constructor does not throw any errors because when it is fully filled with parameters
* it expects the right properties from the database.
*
* Only ::instance and ::create should create instances from this class!
*
* Synced with order\Sortable::__construct
*/
public function __construct($id, $parent = -1, $name = "", $slug = "", $absolute = "", $order = -1, $cnt = 0, $row = array()) {
// Check, if the folder type is defined in the right way
if (!$this instanceof order\Sortable) {
$className = explode("\\", get_class($this));
$className = $className[count($className) - 1];
throw new \Exception("The folder type is defined in the wrong way! Please use the class definition:\n
use " . RML_NS . "\\order; // use namespace
class $className extends order\Sortable { ... }\n\n... You can disable the sortable functionality by set the contentCustomOrder to 2 in the database.");
}
// Set properties
$this->id = $id;
$this->parent = $parent;
$this->name = $name;
$this->cnt = $cnt >= 0 ? $cnt : 0;
$this->order = $order;
$this->children = array();
$this->slug = $slug;
$this->absolutePath = $absolute;
$this->owner = isset($row->owner) ? $row->owner : get_current_user_id();
$this->row = $row;
// Parse the restrictions
if (isset($row->restrictions) && is_string($row->restrictions) && strlen($row->restrictions) > 0) {
$this->restrictions = explode(',', $row->restrictions);
$this->restrictionsCount = count($this->restrictions);
}
}
// documentated in IFolderActions
public function read($order = null, $orderby = null) {
return self::xread($this->id, $order, $orderby);
}
// documentated in IFolderActions
public function relocate($parentId, $nextFolderId = false) {
global $wpdb;
// Collect data
$table_name = $this->getTableName();
$this->debug($parentId === $this->id ? "Start to relocate folder $this->id inside parent..." : "Start to relocate folder $this->id to parent $parentId...", __METHOD__);
$this->debug($nextFolderId === false ? "The folder should take place at the end of the list..." : "The folder should take place before folder id $nextFolderId...", __METHOD__);
$parent = $parentId === $this->id ? $this : wp_rml_get_object_by_id($parentId);
$next = $nextFolderId === false ? null : wp_rml_get_object_by_id($nextFolderId);
// At end of the list
try {
if ($next === null && is_rml_folder($parent)) {
// Only set the parent
$this->setParent($parent->id);
}else if (is_rml_folder($next) && is_rml_folder($parent)) {
// Reindex and reget
$parent->reindexChildrens();
$_this = wp_rml_structure_reset(null, false, $this->id);
$next = wp_rml_get_object_by_id($next->id);
// Get the order of the next folder
$newOrder = $next->order;
// Count up the next ids
$sql = "UPDATE $table_name SET ord = ord + 1 WHERE parent = $parent->id AND ord >= $newOrder";
$wpdb->query($sql);
// Set the new parent
$_this->setParent($parent->id, $newOrder);
}else{
// There is nothing given
throw new \Exception(__("Something went wrong.", RML_TD));
}
$this->debug("Successfully relocated", __METHOD__);
return true;
}catch (\Exception $e) {
$this->debug("Error: " . $e->getMessage(), __METHOD__);
return array($e->getMessage());
}
}
// Documentated in IFolderActions
public function reindexChildrens($resetData = false) {
global $wpdb;
$table_name = $this->getTableName();
$sql = "UPDATE $table_name AS rml2
LEFT JOIN (
SELECT @rownum := @rownum + 1 AS nr, t.ID
FROM ( SELECT rml.id
FROM $table_name AS rml
WHERE rml.parent = $this->id
ORDER BY rml.ord )
AS t, (SELECT @rownum := 0) AS r
) AS rmlnew ON rml2.id = rmlnew.id
SET rml2.ord = rmlnew.nr
WHERE rml2.parent = $this->id";
$wpdb->query($sql);
$this->debug("Reindexed the childrens order of $this->id", __METHOD__);
if ($resetData) {
wp_rml_structure_reset(null, false);
}
}
// Documentated in IFolderActions
public function insert($ids, $supress_validation = false, $isShortcut = false) {
$this->debug("Start moving files " . json_encode($ids) . " to $this->id...", __METHOD__);
if (is_array($ids)) {
// Reset last shortcut ids
if ($isShortcut) {
attachment\Shortcut::getInstance()->_resetLastIds();
}
// Create posts cache to avoid multiple SQL queries in _wp_rml_synchronize_attachment
$cacheIds = array();
foreach ($ids as $value) {
if (!wp_cache_get($value, "posts")) {
$cacheIds[] = $value;
}
}
if (count($cacheIds) > 0) {
$this->debug("Get and cache the following post ids: " . implode(",", $cacheIds), __METHOD__);
get_posts(array(
"numberposts" => -1,
"include" => $cacheIds
));
}
// Iterate all items
foreach ($ids as $value) {
$this->singleCheckInsert($value);
// Check if other fails are counted
if ($supress_validation === false) {
$this->singleCheckInsertPermissions($value);
}
}
/**
* This action is fired before items gets moved to a specific folder.
* It allows you for example to throw an exception with an error message
* to cancel the movement.
*
* @param {int} $fid The destination folder id
* @param {int[]} $attachments The attachment post ids
* @param {IFolder} $folder The folder object
* @param {boolean} $isShortcut If true the attachments are copied to a folder
* @hook RML/Item/Move
*/
do_action("RML/Item/Move", $this->id, $ids, $this, $isShortcut);
// Get the folder IDs of the attachments
$foldersToUpdate = wp_attachment_folder($ids);
// Update the folder
foreach ($ids as $value) {
_wp_rml_synchronize_attachment($value, $this->id, $isShortcut);
}
// Update the count and shortcuts
$foldersToUpdate[] = $this->id;
wp_rml_update_count($foldersToUpdate);
// Finish
$this->debug("Successfully moved (isShortcut: $isShortcut)", __METHOD__);
/**
* This action is fired after items gets moved to a specific folder.
*
* @param {int} $fid The destination folder id
* @param {int[]} $attachments The attachment post ids
* @param {IFolder} $folder The folder object
* @param {boolean} $isShortcut If true the attachments are copied to a folder
* @hook RML/Item/MoveFinished
*/
do_action("RML/Item/MoveFinished", $this->id, $ids, $this, $isShortcut);
return true;
}else{
throw new \Exception(__("You need to provide a set of files.", RML_TD));
}
}
/**
* Simply check, if an id can be inserted in this folder. If something is
* wrong with the id, please throw an exception!
*
* @param int $id The id
* @throws Exception
*/
protected function singleCheckInsertPermissions($id) {
/**
* Checks if an attachment can be inserted into a folder.
*
* @param {string[]} $errors An array of errors
* @param {int} $id The folder id
* @param {IFolder} $folder The folder object
* @hook RML/Validate/Insert
* @returns {string[]} When the array has one or more items the movement is cancelled with the string message
*/
$validation = apply_filters("RML/Validate/Insert", array(), $id, $this);
if (count($validation) > 0) {
throw new \Exception(implode(" ", $validation));
}
}
/**
* Simply check, if an id can be inserted in this folder. If something is
* wrong with the id, please throw an exception!
*
* @param int $id The id
* @throws Exception
*/
protected function singleCheckInsert($id) {
// Silence is golden.
}
/**
* Persist the given creatable with the database. Think about it, that this only
* works, when the ID === -1 (that means, it will be a new folder).
*
* After the folder is created, this instance is useless, you must get the
* folder with the API wp_rml_get_by_id
*
* @throws Exception
* @returns integer ID of the newly created folder
*/
public function persist() {
$this->debug("Persist to database...", __METHOD__);
if ($this->id === -1) {
global $wpdb;
// Check, if the parent exists
$parentObj = wp_rml_get_object_by_id($this->parent);
if (!is_rml_folder($parentObj)) {
throw new \Exception(__("The parent $this->parent does not exist.", RML_TD));
}
// Create it!
$table_name = $this->getTableName();
$insert = $wpdb->insert(
$table_name,
array(
'parent' => $this->parent,
'slug' => $this->getSlug(),
'name' => $this->name,
'type' => $this->getType(),
'ord' => $this->order > -1 ? $this->order : $parentObj->getMaxOrder() + 1,
'restrictions' => implode(",", array_unique($this->restrictions)),
'owner' => $this->owner
)
);
if ($insert !== false) {
$this->id = $wpdb->insert_id;
$this->updateThisAndChildrensAbsolutePath();
wp_rml_structure_reset(null, false);
/**
* A new folder is created.
*
* @param {int} $parent The parent folder id
* @param {string} $name The folder name
* @param {int} $type The folder type
* @param {int} $id The folder id
* @hook RML/Folder/Created
*/
do_action("RML/Folder/Created", $this->parent, $this->name, $this->getType(), $this->id);
$this->debug("Successfully persisted creatable with id " . $this->id, __METHOD__);
return $this->id;
}else{
throw new \Exception(__("The folder could not be created in the database.", RML_TD));
}
}else{
throw new \Exception(__("The folder could not be created because it already exists.", RML_TD));
}
}
// Documentated in IFolderActions
public function updateThisAndChildrensAbsolutePath() {
// Update this absolute path
$this->getAbsolutePath(true, true);
// Update children
$childs = $this->getChildren();
if (is_array($childs) && count($childs) > 0) {
foreach ($childs as $key => $value) {
$value->updateThisAndChildrensAbsolutePath();
}
}
}
/**
* DO NOT USE THIS FUNCTION! IT IS ONLY FOR STRUCTURE PURPOSES.
*/
public function addChildren($children) {
$this->children[] = $children;
}
// Documentated in IFolder
public function getMaxOrder() {
global $wpdb;
$table_name = $this->getTableName();
$order = $wpdb->get_var("SELECT MAX(ord) FROM $table_name WHERE parent=$this->id");
return is_numeric($order) ? $order : 0;
}
// Documentated in IFolder
public function getRowData($field = null) {
if (is_object($this->row)) {
if ($field == null) {
return $this->row;
}else{
return $this->row->$field;
}
}else{
return false;
}
}
// Documentated in IFolder
public function getTypeName($default = null) {
/**
* Filter the description name for a custom folder type.
*
* @param {string} $name The name
* @param {int} $type The type
* @param {int} $fid The folder id
* @returns {string}
* @hook RML/Folder/Type/Name
*/
return apply_filters("RML/Folder/Type/Name", $default === null ? __('Folder', RML_TD) : $default, $this->getType(), $this->getId());
}
// Documentated in IFolder
public function getTypeDescription($default = null) {
/**
* Filter the description for a custom folder type.
*
* @param {string} $description The description
* @param {int} $type The type
* @param {int} $fid The folder id
* @returns {string}
* @hook RML/Folder/Type/Name
*/
return apply_filters("RML/Folder/Type/Description", $default === null ? __('A folder can contain every type of file or a collection, but no gallery.', RML_TD) : $default, $this->getType(), $this->getId());
}
// Documentated in IFolderActions
public function setParent($id, $ord = -1, $force = false) {
// Get the parent id
$this->debug("Try to set parent of $this->id from $this->parent to $id...", __METHOD__);
// Get the parent object
$parent = wp_rml_get_object_by_id($id);
if ($id == $this->parent) {
$this->debug("The parent is the same, propably only the order is changed...", __METHOD__);
}else{
// Check if parent folder is given
if ($parent === null) {
throw new \Exception(__("The given parent does not exist to set the parent for this folder.", RML_TD));
}
// Check if allowed to change the parent
if ($this->isRestrictFor("par")) {
throw new \Exception(__("You are not allowed to change the parent for this folder.", RML_TD));
}
// Check, if the folder type is allowed here
if (!$force && !$parent->isValidChildrenType($this->getType())) {
throw new \Exception(__("The given parent does not allow the folder type.", RML_TD));
}
// Check, if the parent has already the given folder name
if ($parent->hasChildren($this->name)) {
throw new general\FolderAlreadyExistsException($id, $this->name);
}
}
$oldData = $this->getRowData();
$beforeId = $this->parent;
$this->parent = $id;
$this->order = $ord > -1 ? $ord : $parent->getMaxOrder() + 1;
$this->debug("Use $this->order (passed $ord as parameter) as new order value", __METHOD__);
// Save in database
if ($this->id > -1) {
global $wpdb;
// Update childrens
if ($beforeId != $this->parent) {
$this->updateThisAndChildrensAbsolutePath();
}
// Update order
$table_name = $this->getTableName();
$wpdb->query($wpdb->prepare("UPDATE $table_name SET parent=%d, ord=%d WHERE id = %d", $id, $this->order, $this->id));
// Finish
/**
* This action is called when a folder was relocated in the folder tree. That
* means the parent was not changed, only the order was changed.
*
* @param {IFolder} $folder The folder object
* @param {int} $id The folder id
* @param {int} $order The (new) order number
* @param {boolean} $force If true the relocating was forced
* @param {object} $oldData The old SQL row data (raw) of the folder
* @hook RML/Folder/Relocated
*/
/**
* This action is called when a folder was moved in the folder tree. That
* means the parent and order was changed.
*
* @param {IFolder} $folder The folder object
* @param {int} $id The folder id
* @param {int} $order The (new) order number
* @param {boolean} $force If true the relocating was forced
* @param {object} $oldData The old SQL row data (raw) of the folder
* @hook RML/Folder/Moved
*/
do_action($id == $this->id ? 'RML/Folder/Relocated' : 'RML/Folder/Moved', $this, $id, $this->order, $force, $oldData);
$this->debug("Successfully moved and saved in database", __METHOD__);
}else{
$this->debug("Successfully setted the new parent", __METHOD__);
$this->getAbsolutePath(true, true);
}
return true;
}
// Documentated in IFolder
public function setName($name, $supress_validation = false) {
$this->debug("Try to set name of $this->id from '$this->name' to '$name'...", __METHOD__);
// Check valid folder name
if (!$this->isValidName($name)) {
throw new \Exception(sprintf(__("'%s' is not a valid folder name.", RML_TD), $name));
}
// Check, if the parent has already the given folder name
$parent = wp_rml_get_object_by_id($this->parent);
if ($parent !== null && $parent->hasChildren($name)) {
throw new general\FolderAlreadyExistsException($this->parent, $name);
}
if ($supress_validation === false) {
/**
* Checks if a folder can be renamed.
*
* @param {string[]} $errors An array of errors
* @param {string} $name The new folder name
* @param {IFolder} $folder The folder object
* @hook RML/Validate/Rename
* @returns {string[]} When the array has one or more items the rename process is cancelled with the string message
*/
$validation = apply_filters("RML/Validate/Rename", array(), $name, $this);
if (count($validation) > 0) {
throw new \Exception(implode(" ", $validation));
}
}
// Reset
$this->name = $name;
// Save in Database
if ($this->id > -1) {
global $wpdb;
$this->updateThisAndChildrensAbsolutePath();
$oldData = $this->getRowData();
$table_name = $this->getTableName();
$wpdb->query($wpdb->prepare("UPDATE $table_name SET name=%s WHERE id = %d", $name, $this->id));
/**
* This action is called when a folder was renamed.
*
* @param {string} $name The new folder name
* @param {IFolder} $folder The folder object
* @param {object} $oldData The old SQL row data (raw) of the folder
* @hook RML/Folder/Renamed
*/
do_action('RML/Folder/Renamed', $name, $this, $oldData);
$this->debug("Successfully renamed and saved in database", __METHOD__);
}else{
$this->debug("Successfully setted the new name", __METHOD__);
$this->getAbsolutePath(true, true);
}
return true;
}
/**
* Checks, if a given folder name is valid. The name is also santisized so there can
* be no problem for physical moves for example.
*
* @param string $name The folder name
* @returns boolean
*/
public function isValidName($name) {
$name = trim($name);
return /*strpbrk($name, "\\/?%*:|\"<>") === FALSE &&*/ strlen($name) > 0 && !in_array($name, $this->systemReservedFolders);
}
/**
* Read ids for a given folder id.
*
* @param int $id The folder id (-1 for root)
* @param string $order The order
* @param string $orderby The order by
* @returns array with ids
*/
public static function xread($id, $order = null, $orderby = null) {
$args = array(
'post_status' => 'inherit',
'post_type' => 'attachment',
'posts_per_page' => -1,
'rml_folder' => $id,
'fields' => 'ids'
);
// Set orders
if ($order !== null) {
$args["order"] = $order;
}
if ($orderby !== null) {
$args["orderby"] = $orderby;
}
/**
* Modify the query arguments to fetch attachments within a folder.
*
* @param {array} $query The query with post_status, post_type and rml_folder
* @hook RML/Folder/QueryArgs
* @returns {array} The query
*/
$args = apply_filters('RML/Folder/QueryArgs', $args);
$query = new \WP_Query($args);
$posts = $query->get_posts();
/**
* The folder content (attachments) is fetched.
*
* @param {int[]|WP_Post[]} $posts The posts
* @returns {int[]|WP_Post[]}
* @hook RML/Folder/QueryResult
*/
$posts = apply_filters('RML/Folder/QueryResult', $posts);
return $posts;
}
}