* NEW: Add extrafields support to Link class
Link now declares isextrafieldmanaged and wires fetch_optionals()/
insertExtraFields()/deleteExtraFields() into fetch(), fetchAll(),
create(), update() and delete(), matching the llx_links_extrafields
table added previously.
* FIX: Several bugs in Link class review
- create(): wrong duplicate-record error message (copy-pasted from
Societe, referenced undefined $this->name) replaced by the generic
ErrorDuplicateField, consistent with update().
- create()/update(): missing "NoURL" translation key replaced by the
standard ErrorFieldRequired pattern.
- fetch(): guard against running an unfiltered query (no rowid, no
hashforshare) that could silently return an arbitrary link; a caller
in actions_linkedfiles.inc.php could hit this when 'linkid' was
missing from the request.
- actions_linkedfiles.inc.php: check fetch() result with `> 0` instead
of a truthy test, since -1 (error) is truthy in PHP.
- delete(): add User type hint (consistent with create()/update()) and
a $notrigger parameter to optionally skip the LINK_DELETE trigger.
- update(): fix copy-pasted docblock ("third party" -> "link").
* NEW: Add PHPUnit test for Link class
Covers create/fetch/update/fetchAll/count/delete, plus regression
tests for the two bugs fixed in the previous commit: create() rejects
an empty url, and fetch() rejects a call with neither rowid nor
hashforshare instead of returning an arbitrary record.
* FIX Phan false positive on $object in actions_linkedfiles.inc.php
Phan's ambient type inference for the loosely-typed global $object in
this shared include file was picking up CommonSocialNetworks (an
unrelated trait, not even a class), reported as undeclared
->id/->entity/->addThumbs()/->delThumbs() in a real CI Phan run on
this branch, once this file was analyzed on its own via the
changed-files file-list (this file has no prior baseline entry, so it
was apparently never previously exercised in isolation like this).
Force $object's type explicitly to CommonObject via the same
@phan-var-force string-literal idiom this file already uses for
$upload_dir/$upload_dirold/$confirm/$forceFullTextIndexation -
CommonObject genuinely declares addThumbs()/delThumbs(), which
resolves those two errors outright. $id/$entity remain reported as
PhanUndeclaredProperty (CommonObject itself does not declare them,
only its concrete subclasses do at runtime) - baseline-suppress that
for this file the same way it is already suppressed for the sibling
shared-include files actions_addupdatedelete.inc.php,
actions_massactions.inc.php and actions_sendmails.inc.php, which have
the exact same $object typing situation.
473 lines
12 KiB
PHP
473 lines
12 KiB
PHP
<?php
|
|
/* Copyright (C) 2013 Cédric Salvador <csalvador@gpcsolutions.fr>
|
|
* Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
|
|
* Copyright (C) 2024-2026 Frédéric France <frederic.france@free.fr>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/**
|
|
* \file htdocs/core/class/link.class.php
|
|
* \ingroup link
|
|
* \brief File for link class
|
|
*/
|
|
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
|
|
|
|
|
|
/**
|
|
* Class to manage links
|
|
*/
|
|
class Link extends CommonObject
|
|
{
|
|
/**
|
|
* @var string ID to identify managed object
|
|
*/
|
|
public $element = 'link';
|
|
|
|
/**
|
|
* @var string Name of table without prefix where object is stored
|
|
*/
|
|
public $table_element = 'links';
|
|
|
|
/**
|
|
* @var int<0,1> Does object support extrafields ? 0=No, 1=Yes
|
|
*/
|
|
public $isextrafieldmanaged = 1;
|
|
|
|
/**
|
|
* @var int Entity
|
|
*/
|
|
public $entity;
|
|
|
|
/**
|
|
* @var int|'' date add
|
|
*/
|
|
public $datea;
|
|
|
|
/**
|
|
* @var string Object url
|
|
*/
|
|
public $url;
|
|
|
|
/**
|
|
* @var string Links label
|
|
*/
|
|
public $label;
|
|
|
|
/**
|
|
* @var string Object type
|
|
*/
|
|
public $objecttype;
|
|
|
|
/**
|
|
* @var int Object ID
|
|
*/
|
|
public $objectid;
|
|
|
|
/**
|
|
* @var string share hash
|
|
*/
|
|
public $share;
|
|
|
|
/**
|
|
* @var string share pass hash
|
|
*/
|
|
public $share_pass;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param DoliDB $db Database handler
|
|
*/
|
|
public function __construct($db)
|
|
{
|
|
$this->db = $db;
|
|
}
|
|
|
|
|
|
/**
|
|
* Create link in database
|
|
*
|
|
* @param User $user Object of user that ask creation
|
|
* @return int >= 0 if OK, < 0 if KO
|
|
*/
|
|
public function create(User $user)
|
|
{
|
|
global $langs, $conf;
|
|
|
|
$error = 0;
|
|
$langs->loadLangs(array("errors", "admin"));
|
|
// Clean parameters
|
|
if (empty($this->label)) {
|
|
$this->label = trim(basename($this->url));
|
|
}
|
|
if (empty($this->datea)) {
|
|
$this->datea = dol_now();
|
|
}
|
|
$this->url = trim($this->url);
|
|
|
|
dol_syslog(get_class($this)."::create ".$this->url);
|
|
|
|
// Check parameters
|
|
if (empty($this->url)) {
|
|
$this->error = $langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("URL"));
|
|
return -1;
|
|
}
|
|
|
|
$this->db->begin();
|
|
|
|
$sql = "INSERT INTO ".$this->db->prefix()."links (entity, datea, url, label, objecttype, objectid, share,share_pass)";
|
|
$sql .= " VALUES (".((int) $conf->entity).", '".$this->db->idate($this->datea)."'";
|
|
$sql .= ", '".$this->db->escape($this->url)."'";
|
|
$sql .= ", '".$this->db->escape($this->label)."'";
|
|
$sql .= ", '".$this->db->escape($this->objecttype)."'";
|
|
$sql .= ", ".((int) $this->objectid);
|
|
$sql .= ', '.(!empty($this->share) ? "'".$this->db->escape($this->share)."'" : "null");
|
|
$sql .= ', '.(!empty($this->share_pass) ? "'".$this->db->escape($this->share_pass)."'" : "null").")";
|
|
|
|
dol_syslog(get_class($this)."::create", LOG_DEBUG);
|
|
$result = $this->db->query($sql);
|
|
if ($result) {
|
|
$this->id = $this->db->last_insert_id($this->db->prefix()."links");
|
|
|
|
if ($this->id > 0) {
|
|
// Actions on extra fields
|
|
$result = $this->insertExtraFields();
|
|
if ($result < 0) {
|
|
$error++;
|
|
}
|
|
|
|
if (!$error) {
|
|
// Call trigger
|
|
$result = $this->call_trigger('LINK_CREATE', $user);
|
|
if ($result < 0) {
|
|
$error++;
|
|
}
|
|
// End call triggers
|
|
}
|
|
} else {
|
|
$error++;
|
|
}
|
|
|
|
if (!$error) {
|
|
dol_syslog(get_class($this)."::Create success id=".$this->id);
|
|
$this->db->commit();
|
|
return $this->id;
|
|
} else {
|
|
dol_syslog(get_class($this)."::Create echec update ".$this->error, LOG_ERR);
|
|
$this->db->rollback();
|
|
return -3;
|
|
}
|
|
} else {
|
|
if ($this->db->errno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
|
|
$this->error = $langs->trans("ErrorDuplicateField");
|
|
$result = -1;
|
|
} else {
|
|
$this->error = $this->db->lasterror();
|
|
$result = -2;
|
|
}
|
|
$this->db->rollback();
|
|
return $result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update parameters of link
|
|
*
|
|
* @param User $user User executing update
|
|
* @param int $call_trigger 0=no, 1=yes
|
|
* @return int Return integer <0 if KO, >=0 if OK
|
|
*/
|
|
public function update(User $user, $call_trigger = 1)
|
|
{
|
|
global $langs, $conf;
|
|
require_once DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php';
|
|
|
|
$langs->loadLangs(array("errors", "admin"));
|
|
$error = 0;
|
|
|
|
dol_syslog(get_class($this)."::Update id = ".$this->id." call_trigger = ".$call_trigger);
|
|
|
|
// Check parameters
|
|
if (empty($this->url)) {
|
|
$this->error = $langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("URL"));
|
|
return -1;
|
|
}
|
|
|
|
// Clean parameters
|
|
$this->url = clean_url($this->url, 1);
|
|
if (empty($this->label)) {
|
|
$this->label = basename($this->url);
|
|
}
|
|
$this->label = trim($this->label);
|
|
|
|
|
|
$this->db->begin();
|
|
|
|
$sql = "UPDATE ".$this->db->prefix()."links SET ";
|
|
$sql .= "entity = ".((int) $conf->entity);
|
|
$sql .= ", datea = '".$this->db->idate(dol_now())."'";
|
|
$sql .= ", url = '".$this->db->escape($this->url)."'";
|
|
$sql .= ", label = '".$this->db->escape($this->label)."'";
|
|
$sql .= ", objecttype = '".$this->db->escape($this->objecttype)."'";
|
|
$sql .= ", objectid = ".((int) $this->objectid);
|
|
$sql .= ', share = '.(!empty($this->share) ? "'".$this->db->escape($this->share)."'" : "null");
|
|
$sql .= ', share_pass = '.(!empty($this->share_pass) ? "'".$this->db->escape($this->share_pass)."'" : "null");
|
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
|
|
|
dol_syslog(get_class($this)."::update sql = ".$sql);
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
// Actions on extra fields
|
|
$result = $this->insertExtraFields();
|
|
if ($result < 0) {
|
|
$error++;
|
|
}
|
|
|
|
if (!$error && $call_trigger) {
|
|
// Call trigger
|
|
$result = $this->call_trigger('LINK_MODIFY', $user);
|
|
if ($result < 0) {
|
|
$error++;
|
|
}
|
|
// End call triggers
|
|
}
|
|
|
|
if (!$error) {
|
|
dol_syslog(get_class($this)."::Update success");
|
|
$this->db->commit();
|
|
return 1;
|
|
} else {
|
|
setEventMessages('', $this->errors, 'errors');
|
|
$this->db->rollback();
|
|
return -1;
|
|
}
|
|
} else {
|
|
if ($this->db->errno() == 'DB_ERROR_RECORD_ALREADY_EXISTS') {
|
|
// Duplicate
|
|
$this->error = $langs->trans("ErrorDuplicateField");
|
|
$result = -1;
|
|
} else {
|
|
$this->error = $langs->trans("Error sql")."= $sql";
|
|
$result = -2;
|
|
}
|
|
$this->db->rollback();
|
|
return $result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads all links from database
|
|
*
|
|
* @param Link[] $links array of Link objects to fill
|
|
* @param string $objecttype type of the associated object in dolibarr
|
|
* @param int $objectid id of the associated object in dolibarr
|
|
* @param ?string $sortfield field used to sort
|
|
* @param ?string $sortorder sort order
|
|
* @return int<-1,1> 1 if ok, 0 if no records, -1 if error
|
|
*/
|
|
public function fetchAll(&$links, $objecttype, $objectid, $sortfield = null, $sortorder = null)
|
|
{
|
|
global $conf;
|
|
|
|
$sql = "SELECT rowid, entity, datea, url, label, objecttype, objectid, share,share_pass FROM ".$this->db->prefix()."links";
|
|
$sql .= " WHERE objecttype = '".$this->db->escape($objecttype)."' AND objectid = ".((int) $objectid);
|
|
if ($conf->entity != 0) {
|
|
$sql .= " AND entity = ".((int) $conf->entity);
|
|
}
|
|
if ($sortfield) {
|
|
if (empty($sortorder)) {
|
|
$sortorder = "ASC";
|
|
}
|
|
$sql .= $this->db->order($sortfield, $sortorder);
|
|
}
|
|
|
|
dol_syslog(get_class($this)."::fetchAll", LOG_DEBUG);
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
$num = $this->db->num_rows($resql);
|
|
dol_syslog(get_class($this)."::fetchAll num=".((int) $num), LOG_DEBUG);
|
|
if ($num > 0) {
|
|
while ($obj = $this->db->fetch_object($resql)) {
|
|
$link = new Link($this->db);
|
|
$link->id = (int) $obj->rowid;
|
|
$link->entity = $obj->entity;
|
|
$link->datea = $this->db->jdate($obj->datea);
|
|
$link->url = $obj->url;
|
|
$link->label = $obj->label;
|
|
$link->objecttype = $obj->objecttype;
|
|
$link->objectid = $obj->objectid;
|
|
$link->share = $obj->share;
|
|
$link->share_pass = $obj->share_pass;
|
|
|
|
// Retrieve all extrafields for link
|
|
$link->fetch_optionals();
|
|
|
|
$links[] = $link;
|
|
}
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return nb of links
|
|
*
|
|
* @param DoliDB $dbs Database handler
|
|
* @param string $objecttype Type of the associated object in dolibarr
|
|
* @param int $objectid Id of the associated object in dolibarr
|
|
* @return int Nb of links, -1 if error
|
|
**/
|
|
public static function count($dbs, $objecttype, $objectid)
|
|
{
|
|
global $conf;
|
|
|
|
$sql = "SELECT COUNT(rowid) as nb FROM ".$dbs->prefix()."links";
|
|
$sql .= " WHERE objecttype = '".$dbs->escape($objecttype)."' AND objectid = ".((int) $objectid);
|
|
if ($conf->entity != 0) {
|
|
$sql .= " AND entity = ".((int) $conf->entity);
|
|
}
|
|
|
|
$resql = $dbs->query($sql);
|
|
if ($resql) {
|
|
$obj = $dbs->fetch_object($resql);
|
|
if ($obj) {
|
|
return $obj->nb;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Loads a link from database
|
|
*
|
|
* @param int $rowid Id of link to load
|
|
* @param string $hashforshare Hash of file sharing, or 'shared'
|
|
* @return int 1 if ok, 0 if no record found, -1 if error
|
|
**/
|
|
public function fetch($rowid = null, $hashforshare = '')
|
|
{
|
|
global $conf;
|
|
|
|
if (empty($rowid)) {
|
|
$rowid = $this->id;
|
|
}
|
|
|
|
if (empty($rowid) && empty($hashforshare)) {
|
|
$this->error = 'ErrorBadParameters';
|
|
return -1;
|
|
}
|
|
|
|
$sqlwhere = [];
|
|
|
|
$sql = "SELECT rowid, entity, datea, url, label, objecttype, objectid, share, share_pass FROM ".$this->db->prefix()."links";
|
|
if (!empty((int) $rowid)) {
|
|
$sqlwhere[] = " rowid = ".((int) $rowid);
|
|
}
|
|
if (!empty($hashforshare)) {
|
|
$sqlwhere[] = " share = '".$this->db->escape($hashforshare)."'";
|
|
}
|
|
|
|
if ($conf->entity != 0) {
|
|
$sqlwhere[] = " entity = ".((int) $conf->entity);
|
|
}
|
|
if (count($sqlwhere) > 0) {
|
|
$sql .= ' WHERE '.implode(' AND ', $sqlwhere);
|
|
}
|
|
|
|
dol_syslog(get_class($this)."::fetch", LOG_DEBUG);
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
if ($this->db->num_rows($resql) > 0) {
|
|
$obj = $this->db->fetch_object($resql);
|
|
|
|
$this->id = $obj->rowid;
|
|
$this->entity = $obj->entity;
|
|
$this->datea = $this->db->jdate($obj->datea);
|
|
$this->url = $obj->url;
|
|
$this->label = $obj->label;
|
|
$this->objecttype = $obj->objecttype;
|
|
$this->objectid = $obj->objectid;
|
|
$this->share = $obj->share;
|
|
$this->share_pass = $obj->share_pass;
|
|
|
|
// Retrieve all extrafields for link
|
|
$this->fetch_optionals();
|
|
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
} else {
|
|
$this->error = $this->db->lasterror();
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a link from database
|
|
*
|
|
* @param User $user Object user
|
|
* @param int<0,1> $notrigger 1=Does not execute triggers, 0=Execute triggers
|
|
* @return int Return integer <0 if KO, 0 if nothing done, >0 if OK
|
|
*/
|
|
public function delete(User $user, $notrigger = 0)
|
|
{
|
|
dol_syslog(get_class($this)."::delete", LOG_DEBUG);
|
|
$error = 0;
|
|
|
|
$this->db->begin();
|
|
|
|
if (!$notrigger) {
|
|
// Call trigger
|
|
$result = $this->call_trigger('LINK_DELETE', $user);
|
|
if ($result < 0) {
|
|
$this->db->rollback();
|
|
return -1;
|
|
}
|
|
// End call triggers
|
|
}
|
|
|
|
// Remove link
|
|
$sql = "DELETE FROM ".$this->db->prefix()."links";
|
|
$sql .= " WHERE rowid = ".((int) $this->id);
|
|
|
|
dol_syslog(get_class($this)."::delete", LOG_DEBUG);
|
|
if (!$this->db->query($sql)) {
|
|
$error++;
|
|
$this->error = $this->db->lasterror();
|
|
}
|
|
|
|
// Removed extrafields
|
|
if (!$error) {
|
|
$result = $this->deleteExtraFields();
|
|
if ($result < 0) {
|
|
$error++;
|
|
}
|
|
}
|
|
|
|
if (!$error) {
|
|
$this->db->commit();
|
|
|
|
return 1;
|
|
} else {
|
|
$this->db->rollback();
|
|
return -1;
|
|
}
|
|
}
|
|
}
|