diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index a13037402f8..19c076242f8 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -115,12 +115,14 @@ Also, some code changes need a prior approbation:
Once a PR has been submitted, you may need to wait for its integration. It is common that the project leader let the PR open for a long delay to allow every developer discuss about the PR.
+If the label of PR start with "WIP" (Work In Progress), it will not be analyzed (until you change the label of PR).
+
If your PR has errors reported by the Continuous Integration Platform, it means your PR is not valid and nothing will be done with it. It will be kept open to allow developers to fix this, or it may be closed several month later. Don't expect anything on your PR if you have such errors, you MUST first fix the Continuous Integration error to have it taken into consideration.
If the PR is valid, and is kept open for a long time, a tag will also be added on the PR to describe the status of your PR and why the PR is kept open. By putting your mouse on the tag, you will get a full explanation of the tag/status that explain why your PR has not been integrated yet.
-In most cases, it give you information of things you have to do to have the PR taken into consideration (for example a change is requested, a conflict is expected to be solved, some questions were asked). If you have a yellow, red flag of purple flag, don't expect to have your PR validated. You must first provide the answer the flag ask you. The majority of PR are waiting a developer action.
+In most cases, it gives you information of things you have to do to have the PR taken into consideration (for example a change is requested, a conflict is expected to be solved, some questions were asked). If you have a yellow, red flag of purple flag, don't expect to have your PR validated. You must first provide the answer the flag ask you. The majority of PR are waiting an action of the developer/author.
-Around 95% of submitted PR are reviewed and tagged. Even if this is one of the most important ratio of answered PR in Open Source world, don't expect the core team to reach the 100%. With the increasing popularity of Dolibarr, this ratio will probably decrease in future.
+Statistics on Dolibarr project shows that around 95% of submitted PR are reviewed and tagged. This is one of the most important ratio of answered PR in Open Source world. Don't expect the core team to reach the 100%. With the increasing popularity of Dolibarr, this ratio will probably decrease in future.
### Resources
diff --git a/ChangeLog b/ChangeLog
index c425872e9c2..0839fd77e95 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -15,8 +15,117 @@ WARNING:
Following changes may create regressions for some external modules, but were necessary to make Dolibarr better:
* Properties ->libelle_incoterms were renamed into ->label_incoterms
* Removed the method liste_array() of project class. It was not used by core code.
+* The function show_theme() hase been renamed into showSkins()
+***** ChangeLog for 10.0.1 compared to 10.0.0 *****
+FIX: #10930
+FIX: #10984
+FIX: reposition on "Build backup" button
+FIX: #11400
+FIX: #11412
+FIX: #11460
+FIX: #11463
+FIX: #11466
+FIX: #11492
+FIX: #11498
+FIX: #11505
+FIX: #11506
+FIX: #11507
+FIX: #11509
+FIX: #11537
+FIX: #11543
+FIX: #11553
+FIX: #11576
+FIX: #11584
+FIX: #11590
+FIX: accounting mode must be taken from global conf, because there's no way to choose a mode with interface
+FIX: Add message from public interface
+FIX: add missing hook calls
+FIX: Add warning when setup is strange
+FIX: ajax call for line positioning when CSRFCHECK_WITH_TOKEN is on
+FIX: API return 404 sometimes even if API exists
+FIX: Attachment was lost when we validate an expense report
+FIX: avoid conflict with "$classname" in card.php
+FIX: Bad sql request
+FIX: better compatibility with multicompany transverse mode
+FIX: Better PHP compatibility
+FIX: Block to link with tickets
+FIX: Can't submit a ticket from public interface
+FIX: categories import: prevent mismatch between category type and object type
+FIX: Closing ticket from public interface
+FIX: Column 'paid' missing in expense report
+FIX: compatibility mysql 8. rank is reserved
+FIX: Computed field were not calculated into lists.
+FIX: Content of email for subscription
+FIX: correct error in files with multiple spaces
+FIX: CVE-2019-11199
+FIX: delete of links between objects
+FIX: div not balanced
+FIX: do not return formatted prices in json string
+FIX: duplicate on the check (TODO field $onetrtd not used ?)
+FIX: element name in update_price
+FIX: empty product_use_units in product configuration
+FIX: expedition card: infinite loop for printObjectLine hook if return > 0
+FIX: extrafield loading bug due to assumption that an object is a third party while it may be a contact if MAIN_USE_COMPANY_NAME_OF_CONTACT is set.
+FIX: Fatal error on dol_htmloutput_mesg with corrupted array
+FIX: Fatal situation if payment removed on expense report. Action
+FIX: FEC Format - Missing date_creation in general ledger when you add a new transaction
+FIX: FEC Format - Save translation of the journal label in database & nowrap on amount
+FIX: floating point precision errors in the triggers of the workflow module
+FIX: for #11232
+FIX: format of field with type timestamp
+FIX: fournrprice log for insert
+FIX: help text
+FIX: import filter error
+FIX: __INFOS__ tag not exists
+FIX: issue #9300: install error with PostgreSQL when using custom table prefix
+FIX: Language key
+FIX: Limit of uploaded files (max_post_size was not used)
+FIX: list of balance of leaves
+FIX: minor spelling issues
+FIX: missing "dropdown-icon" replacement
+FIX: Missing field "Conciliated" into bank transaction export
+FIX: missing filter by current contact
+FIX: missing token
+FIX: Missing where on entity
+FIX: move sql request in INNER JOIN
+FIX: name was able to be in field but went back to new line
+FIX: Nowrap on amount
+FIX: Online payment
+FIX: on shipment delete confirm dialog, a new checkbox allows the user to choose if they want their stock re-incremented after the deletion.
+FIX: option EXPORT_LABEL_FOR_SELECT to restore compatibility in export
+FIX: Option THIRDPARTY_SUGGEST_ALSO_ADDRESS_CREATION
+FIX: outdated phpdoc
+FIX: Permission for BOM menu
+FIX: permission to delete a draft purchase order
+FIX: phpcs
+FIX: Position was lost when we edit the line of template invoice
+FIX: product_use_units was set to 0 each time a conf in block other was set
+FIX: propal createFrom hook: undefined parameter attached
+FIX: Responsive of public interface of ticket
+FIX: search by phone pro
+FIX: Setup of TakePos was not possible after a clean install
+FIX: Show list of events on tickets
+FIX: socpeople assigned list in action com list
+FIX: SQL problem on donation & nowrap on amount
+FIX: stock increase on shipment deletion if STOCK_CALCULATE_ON_SHIPMENT_NEW: is set
+FIX: stripe webhook ID constant set
+FIX: summary of time spent in preview tab of projects
+FIX: the feature to bill time spent was not enabled.
+FIX: The new feature to attach document on lines was not correclty
+FIX: The proposed new supplier code does not work
+FIX: this function can not be private
+FIX: tk9877 - PDF rouget requires product.lib.php (otherwise measuring_units_string() is not defined)
+FIX: Update the file index table when we validate/rename a ref.
+FIX: use rounding to compare the amounts
+FIX: We must save code instead of value in database for template invoice modelpdf
+FIX: we need to be able to add freeline with qty between 0 & 1 in supplierorder line
+FIX: We should remove property comments only for project and task api.
+FIX: When saving an action it didn't save the label based on the type of event if the label is empty and the type is customized
+FIX: when STOCK_CALCULATE_ON_SHIPMENT_NEW: is set, deleting a "closed" shipment now increases stock as expected
+FIX: wrong path sociales/index.php doesnt exist anymore
+
***** ChangeLog for 10.0.0 compared to 9.0.0 *****
For Users:
NEW: Module "Ticket" is available as a stable module.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000000..e5493805733
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,13 @@
+# Security Policy
+
+## Supported Versions
+
+| Version | Supported |
+| -------- | ------------------ |
+| <= 8.0.* | :x: |
+| >= 9.0.* | :white_check_mark: |
+
+## Reporting a Vulnerability
+
+To report a vulnerability, please send an email to security@dolibarr.org
+In most cases, after fixing the security, we make an answer by email to say the issue has been fixed.
diff --git a/build/exe/doliwamp/php.ini.install b/build/exe/doliwamp/php.ini.install
index 04191a71f5a..af8ef607112 100644
--- a/build/exe/doliwamp/php.ini.install
+++ b/build/exe/doliwamp/php.ini.install
@@ -458,16 +458,6 @@ variables_order = "GPCS"
; with user data. This makes most sense when coupled with track_vars - in which
; case you can access all of the GPC variables through the $HTTP_*_VARS[],
; variables.
-;
-; You should do your best to write your scripts so that they do not require
-; register_globals to be on; Using form variables as globals can easily lead
-; to possible security problems, if the code is not very well thought of.
-register_globals = Off
-
-; Whether or not to register the old-style input arrays, HTTP_GET_VARS
-; and friends. If you're not using them, it's recommended to turn them off,
-; for performance reasons.
-register_long_arrays = Off
; This directive tells PHP whether to declare the argv&argc variables (that
; would contain the GET information). If you don't use these variables, you
@@ -477,8 +467,7 @@ register_argc_argv = Off
; When enabled, the SERVER and ENV variables are created when they're first
; used (Just In Time) instead of when the script starts. If these variables
; are not used within a script, having this directive on will result in a
-; performance gain. The PHP directives register_globals, register_long_arrays,
-; and register_argc_argv must be disabled for this directive to have any affect.
+; performance gain.
auto_globals_jit = On
; Maximum size of POST data that PHP will accept.
@@ -1101,14 +1090,6 @@ session.gc_maxlifetime = 1800
; setting session.gc_maxlifetime to 1440 (1440 seconds = 24 minutes):
; cd /path/to/sessions; find -cmin +24 | xargs rm
-; PHP 4.2 and less have an undocumented feature/bug that allows you to
-; to initialize a session variable in the global scope, albeit register_globals
-; is disabled. PHP 4.3 and later will warn you, if this feature is used.
-; You can disable the feature and the warning separately. At this time,
-; the warning is only displayed, if bug_compat_42 is enabled.
-
-session.bug_compat_42 = 0
-session.bug_compat_warn = 1
; Check HTTP Referer to invalidate externally stored URLs containing ids.
; HTTP_REFERER has to contain this substring for the session to be
diff --git a/build/rpm/httpd-dolibarr.conf b/build/rpm/httpd-dolibarr.conf
index 1126d4fe442..ebda2b3ddfc 100644
--- a/build/rpm/httpd-dolibarr.conf
+++ b/build/rpm/httpd-dolibarr.conf
@@ -29,16 +29,6 @@ Alias /dolibarr /usr/share/dolibarr/htdocs
ErrorDocument 401 /public/error-401.php
ErrorDocument 404 /public/error-404.php
-
- php_flag magic_quotes_gpc Off
- php_flag register_globals Off
-
-
-
- php_flag magic_quotes_gpc Off
- php_flag register_globals Off
-
-
# OPTIMIZE: To use gzip compressed files (for Dolibarr already compressed files).
# Note that constant MAIN_OPTIMIZE_SPEED must have a value with bit 0 set.
diff --git a/dev/setup/codesniffer/php.ini b/dev/setup/codesniffer/php.ini
index 6f374340edd..00f3b2d4efa 100644
--- a/dev/setup/codesniffer/php.ini
+++ b/dev/setup/codesniffer/php.ini
@@ -658,20 +658,6 @@ html_errors = Off
; Example:
;arg_separator.input = ";&"
-; This directive determines which super global arrays are registered when PHP
-; starts up. If the register_globals directive is enabled, it also determines
-; what order variables are populated into the global space. G,P,C,E & S are
-; abbreviations for the following respective super globals: GET, POST, COOKIE,
-; ENV and SERVER. There is a performance penalty paid for the registration of
-; these arrays and because ENV is not as commonly used as the others, ENV is
-; is not recommended on productions servers. You can still get access to
-; the environment variables through getenv() should you need to.
-; Default Value: "EGPCS"
-; Development Value: "GPCS"
-; Production Value: "GPCS";
-; http://php.net/variables-order
-variables_order = "GPCS"
-
; This directive determines which super global data (G,P,C,E & S) should
; be registered into the super global array REQUEST. If so, it also determines
; the order in which that data is registered. The values for this directive are
@@ -685,15 +671,6 @@ variables_order = "GPCS"
; http://php.net/request-order
request_order = "GP"
-; Whether or not to register the EGPCS variables as global variables. You may
-; want to turn this off if you don't want to clutter your scripts' global scope
-; with user data.
-; You should do your best to write your scripts so that they do not require
-; register_globals to be on; Using form variables as globals can easily lead
-; to possible security problems, if the code is not very well thought of.
-; http://php.net/register-globals
-register_globals = Off
-
; Determines whether the deprecated long $HTTP_*_VARS type predefined variables
; are registered by PHP or not. As they are deprecated, we obviously don't
; recommend you use them. They are on by default for compatibility reasons but
@@ -722,8 +699,7 @@ register_argc_argv = Off
; When enabled, the SERVER and ENV variables are created when they're first
; used (Just In Time) instead of when the script starts. If these variables
; are not used within a script, having this directive on will result in a
-; performance gain. The PHP directives register_globals, register_long_arrays,
-; and register_argc_argv must be disabled for this directive to have any affect.
+; performance gain.
; http://php.net/auto-globals-jit
auto_globals_jit = On
@@ -1516,22 +1492,6 @@ session.gc_maxlifetime = 1440
; setting session.gc_maxlifetime to 1440 (1440 seconds = 24 minutes):
; find /path/to/sessions -cmin +24 | xargs rm
-; PHP 4.2 and less have an undocumented feature/bug that allows you to
-; to initialize a session variable in the global scope, even when register_globals
-; is disabled. PHP 4.3 and later will warn you, if this feature is used.
-; You can disable the feature and the warning separately. At this time,
-; the warning is only displayed, if bug_compat_42 is enabled. This feature
-; introduces some serious security problems if not handled correctly. It's
-; recommended that you do not use this feature on production servers. But you
-; should enable this on development servers and enable the warning as well. If you
-; do not enable the feature on development servers, you won't be warned when it's
-; used and debugging errors caused by this can be difficult to track down.
-; Default Value: On
-; Development Value: On
-; Production Value: Off
-; http://php.net/session.bug-compat-42
-session.bug_compat_42 = Off
-
; This setting controls whether or not you are warned by PHP when initializing a
; session value into the global space. session.bug_compat_42 must be enabled before
; these warnings can be issued by PHP. See the directive above for more information.
diff --git a/dev/setup/codesniffer/ruleset.xml b/dev/setup/codesniffer/ruleset.xml
index daf73e5b2a7..89ea98a051b 100644
--- a/dev/setup/codesniffer/ruleset.xml
+++ b/dev/setup/codesniffer/ruleset.xml
@@ -6,6 +6,7 @@
build/html
build/aps
documents
+ htdocs/core/class/lessc.class.php
htdocs/custom
htdocs/includes
htdocs/install/doctemplates/websites
diff --git a/htdocs/accountancy/admin/accountmodel.php b/htdocs/accountancy/admin/accountmodel.php
index 527cb13fcfd..87a7464dda6 100644
--- a/htdocs/accountancy/admin/accountmodel.php
+++ b/htdocs/accountancy/admin/accountmodel.php
@@ -156,17 +156,8 @@ if (GETPOST('actionadd', 'alpha') || GETPOST('actionmodify', 'alpha'))
$ok=1;
foreach ($listfield as $f => $value)
{
- if ($value == 'country_id' && in_array($tablib[$id], array('DictionaryVAT','DictionaryRegion','DictionaryCompanyType','DictionaryHolidayTypes','DictionaryRevenueStamp','DictionaryAccountancyCategory','Pcg_version'))) continue; // For some pages, country is not mandatory
- if ($value == 'country' && in_array($tablib[$id], array('DictionaryCanton','DictionaryCompanyType','DictionaryRevenueStamp'))) continue; // For some pages, country is not mandatory
- if ($value == 'localtax1' && empty($_POST['localtax1_type'])) continue;
- if ($value == 'localtax2' && empty($_POST['localtax2_type'])) continue;
- if ($value == 'color' && empty($_POST['color'])) continue;
- if ($value == 'formula' && empty($_POST['formula'])) continue;
- if ((! isset($_POST[$value]) || $_POST[$value]=='')
- && (! in_array($listfield[$f], array('decalage','module','accountancy_code','accountancy_code_sell','accountancy_code_buy')) // Fields that are not mandatory
- && (! ($id == 10 && $listfield[$f] == 'code')) // Code is mandatory fir table 10
- )
- )
+ if ($value == 'country_id' && in_array($tablib[$id], array('Pcg_version'))) continue; // For some pages, country is not mandatory
+ if ((! isset($_POST[$value]) || $_POST[$value]==''))
{
$ok=0;
$fieldnamekey=$listfield[$f];
@@ -174,19 +165,6 @@ if (GETPOST('actionadd', 'alpha') || GETPOST('actionmodify', 'alpha'))
if ($fieldnamekey == 'pcg_version') $fieldnamekey='Pcg_version';
if ($fieldnamekey == 'libelle' || ($fieldnamekey == 'label')) $fieldnamekey='Label';
- if ($fieldnamekey == 'libelle_facture') $fieldnamekey = 'LabelOnDocuments';
- if ($fieldnamekey == 'nbjour') $fieldnamekey='NbOfDays';
- if ($fieldnamekey == 'decalage') $fieldnamekey='Offset';
- if ($fieldnamekey == 'module') $fieldnamekey='Module';
- if ($fieldnamekey == 'code') $fieldnamekey = 'Code';
- if ($fieldnamekey == 'note') $fieldnamekey = 'Note';
- if ($fieldnamekey == 'taux') $fieldnamekey = 'Rate';
- if ($fieldnamekey == 'type') $fieldnamekey = 'Type';
- if ($fieldnamekey == 'position') $fieldnamekey = 'Position';
- if ($fieldnamekey == 'unicode') $fieldnamekey = 'Unicode';
- if ($fieldnamekey == 'deductible') $fieldnamekey = 'Deductible';
- if ($fieldnamekey == 'sortorder') $fieldnamekey = 'SortOrder';
- if ($fieldnamekey == 'category_type') $fieldnamekey = 'Calculated';
setEventMessages($langs->transnoentities("ErrorFieldRequired", $langs->transnoentities($fieldnamekey)), null, 'errors');
}
@@ -196,9 +174,9 @@ if (GETPOST('actionadd', 'alpha') || GETPOST('actionmodify', 'alpha'))
$ok=0;
setEventMessages($langs->transnoentities('ErrorReservedTypeSystemSystemAuto'), null, 'errors');
}
- if (isset($_POST["code"]))
+ if (isset($_POST["pcg_version"]))
{
- if ($_POST["code"]=='0')
+ if ($_POST["pcg_version"]=='0')
{
$ok=0;
setEventMessages($langs->transnoentities('ErrorCodeCantContainZero'), null, 'errors');
@@ -211,28 +189,9 @@ if (GETPOST('actionadd', 'alpha') || GETPOST('actionmodify', 'alpha'))
}
if (isset($_POST["country"]) && ($_POST["country"]=='0') && ($id != 2))
{
- if (in_array($tablib[$id], array('DictionaryCompanyType','DictionaryHolidayTypes'))) // Field country is no mandatory for such dictionaries
- {
- $_POST["country"]='';
- }
- else
- {
- $ok=0;
- setEventMessages($langs->transnoentities("ErrorFieldRequired", $langs->transnoentities("Country")), null, 'errors');
- }
+ $ok=0;
+ setEventMessages($langs->transnoentities("ErrorFieldRequired", $langs->transnoentities("Country")), null, 'errors');
}
- if (! is_numeric($_POST["code"]))
- {
- $ok=0;
- setEventMessages($langs->transnoentities("ErrorFieldMustBeANumeric", $langs->transnoentities("Code")), null, 'errors');
- }
-
- // Clean some parameters
- if (isset($_POST["localtax1"]) && empty($_POST["localtax1"])) $_POST["localtax1"]='0'; // If empty, we force to 0
- if (isset($_POST["localtax2"]) && empty($_POST["localtax2"])) $_POST["localtax2"]='0'; // If empty, we force to 0
- if ($_POST["accountancy_code"] <= 0) $_POST["accountancy_code"]=''; // If empty, we force to null
- if ($_POST["accountancy_code_sell"] <= 0) $_POST["accountancy_code_sell"]=''; // If empty, we force to null
- if ($_POST["accountancy_code_buy"] <= 0) $_POST["accountancy_code_buy"]=''; // If empty, we force to null
// Si verif ok et action add, on ajoute la ligne
if ($ok && GETPOST('actionadd', 'alpha'))
diff --git a/htdocs/admin/dict.php b/htdocs/admin/dict.php
index 87e072b8d7e..f68864fcc3d 100644
--- a/htdocs/admin/dict.php
+++ b/htdocs/admin/dict.php
@@ -1058,7 +1058,7 @@ if ($id)
$valuetoshow=$langs->trans($valuetoshow); // try to translate
$class='';
- if ($fieldlist[$field]=='pos') { $valuetoshow=$langs->trans("Position"); $class='width100'; }
+ if ($fieldlist[$field]=='pos') { $valuetoshow=$langs->trans("Position"); $class='maxwidth100'; }
if ($fieldlist[$field]=='source') { $valuetoshow=$langs->trans("Contact"); }
if ($fieldlist[$field]=='price') { $valuetoshow=$langs->trans("PriceUHT"); }
if ($fieldlist[$field]=='taux') {
@@ -1076,7 +1076,7 @@ if ($id)
if ($tabname[$id] == MAIN_DB_PREFIX."c_paiement") $valuetoshow=$form->textwithtooltip($langs->trans("Type"), $langs->trans("TypePaymentDesc"), 2, 1, img_help(1, ''));
else $valuetoshow=$langs->trans("Type");
}
- if ($fieldlist[$field]=='code') { $valuetoshow=$langs->trans("Code"); $class='width100'; }
+ if ($fieldlist[$field]=='code') { $valuetoshow=$langs->trans("Code"); $class='maxwidth100'; }
if ($fieldlist[$field]=='libelle' || $fieldlist[$field]=='label')
{
$valuetoshow=$form->textwithtooltip($langs->trans("Label"), $langs->trans("LabelUsedByDefault"), 2, 1, img_help(1, ''));
@@ -1602,7 +1602,6 @@ if ($id)
$class='tddict';
if ($fieldlist[$field] == 'note' && $id == 10) $class.=' tdoverflowmax200';
if ($fieldlist[$field] == 'tracking') $class.=' tdoverflowauto';
- if ($fieldlist[$field] == 'code') $class.=' width100';
if ($fieldlist[$field] == 'position') $class.=' right';
if ($fieldlist[$field] == 'localtax1_type') $class.=' nowrap';
if ($fieldlist[$field] == 'localtax2_type') $class.=' nowrap';
@@ -1780,7 +1779,6 @@ function fieldList($fieldlist, $obj = '', $tabname = '', $context = '')
global $form;
global $region_id;
global $elementList,$sourceList,$localtax_typeList;
- global $bc;
$formadmin = new FormAdmin($db);
$formcompany = new FormCompany($db);
@@ -1981,7 +1979,7 @@ function fieldList($fieldlist, $obj = '', $tabname = '', $context = '')
}
$classtd=''; $class='';
- if ($fieldlist[$field]=='code') $classtd='width100';
+ if ($fieldlist[$field]=='code') $class='maxwidth100';
if (in_array($fieldlist[$field], array('pos', 'use_default', 'affect', 'delay', 'position', 'sortorder', 'sens', 'category_type'))) $class='maxwidth50';
if (in_array($fieldlist[$field], array('libelle', 'label', 'tracking'))) $class='quatrevingtpercent';
print '
';
// Ref customer
- $morehtmlref.=$form->editfieldkey("RefCustomer", 'ref_client', $object->ref_client, $object, $user->rights->commande->creer, 'string', '', 0, 1);
- $morehtmlref.=$form->editfieldval("RefCustomer", 'ref_client', $object->ref_client, $object, $user->rights->commande->creer, 'string', '', null, null, '', 1);
+ $morehtmlref.=$form->editfieldkey("RefCustomer", 'ref_client', $object->ref_client, $object, $usercancreate, 'string', '', 0, 1);
+ $morehtmlref.=$form->editfieldval("RefCustomer", 'ref_client', $object->ref_client, $object, $usercancreate, 'string', '', null, null, '', 1);
// Thirdparty
$morehtmlref.='
'.$langs->trans('ThirdParty') . ' : ' . $soc->getNomUrl(1);
if (empty($conf->global->MAIN_DISABLE_OTHER_LINK) && $object->thirdparty->id > 0) $morehtmlref.=' (
'.$langs->trans("OtherOrders").' )';
@@ -2079,7 +2081,7 @@ if ($action == 'create' && $user->rights->commande->creer)
{
$langs->load("projects");
$morehtmlref.='
'.$langs->trans('Project') . ' ';
- if ($user->rights->commande->creer)
+ if ($usercancreate)
{
if ($action != 'classify')
$morehtmlref.='
' . img_edit($langs->transnoentitiesnoconv('SetProject')) . ' : ';
@@ -2159,7 +2161,7 @@ if ($action == 'create' && $user->rights->commande->creer)
// Date
print '
';
- $editenable = $user->rights->commande->creer && $object->statut == Commande::STATUS_DRAFT;
+ $editenable = $usercancreate && $object->statut == Commande::STATUS_DRAFT;
print $form->editfieldkey("Date", 'date', '', $object, $editenable);
print ' ';
if ($action == 'editdate') {
@@ -2180,7 +2182,7 @@ if ($action == 'create' && $user->rights->commande->creer)
// Delivery date planed
print ' ';
- $editenable = $user->rights->commande->creer;
+ $editenable = $usercancreate;
print $form->editfieldkey("DateDeliveryPlanned", 'date_livraison', '', $object, $editenable);
print ' ';
if ($action == 'editdate_livraison') {
@@ -2202,7 +2204,7 @@ if ($action == 'create' && $user->rights->commande->creer)
// Shipping Method
if (! empty($conf->expedition->enabled)) {
print ' ';
- $editenable = $user->rights->commande->creer;
+ $editenable = $usercancreate;
print $form->editfieldkey("SendingMethod", 'shippingmethod', '', $object, $editenable);
print ' ';
if ($action == 'editshippingmethod') {
@@ -2220,7 +2222,7 @@ if ($action == 'create' && $user->rights->commande->creer)
require_once DOL_DOCUMENT_ROOT.'/product/class/html.formproduct.class.php';
$formproduct=new FormProduct($db);
print ' ';
- $editenable = $user->rights->commande->creer;
+ $editenable = $usercancreate;
print $form->editfieldkey("Warehouse", 'warehouse', '', $object, $editenable);
print ' ';
if ($action == 'editwarehouse') {
@@ -2234,7 +2236,7 @@ if ($action == 'create' && $user->rights->commande->creer)
// Terms of payment
print ' ';
- $editenable = $user->rights->commande->creer;
+ $editenable = $usercancreate;
print $form->editfieldkey("PaymentConditionsShort", 'conditions', '', $object, $editenable);
print ' ';
if ($action == 'editconditions') {
@@ -2248,7 +2250,7 @@ if ($action == 'create' && $user->rights->commande->creer)
// Mode of payment
print ' ';
- $editenable = $user->rights->commande->creer;
+ $editenable = $usercancreate;
print $form->editfieldkey("PaymentMode", 'mode', '', $object, $editenable);
print ' ';
if ($action == 'editmode') {
@@ -2264,7 +2266,7 @@ if ($action == 'create' && $user->rights->commande->creer)
// Multicurrency code
print ' ';
print '';
- $editenable = $user->rights->commande->creer && $object->statut == Commande::STATUS_DRAFT;
+ $editenable = $usercancreate && $object->statut == Commande::STATUS_DRAFT;
print $form->editfieldkey("Currency", 'multicurrencycode', '', $object, $editenable);
print ' ';
if ($action == 'editmulticurrencycode') {
@@ -2277,7 +2279,7 @@ if ($action == 'create' && $user->rights->commande->creer)
// Multicurrency rate
print ' ';
print '';
- $editenable = $user->rights->commande->creer && $object->multicurrency_code && $object->multicurrency_code != $conf->currency && $object->statut == Commande::STATUS_DRAFT;
+ $editenable = $usercancreate && $object->multicurrency_code && $object->multicurrency_code != $conf->currency && $object->statut == Commande::STATUS_DRAFT;
print $form->editfieldkey("CurrencyRate", 'multicurrencyrate', '', $object, $editenable);
print ' ';
if ($action == 'editmulticurrencyrate' || $action == 'actualizemulticurrencyrate') {
@@ -2298,7 +2300,7 @@ if ($action == 'create' && $user->rights->commande->creer)
// Delivery delay
print ' ';
- $editenable = $user->rights->commande->creer;
+ $editenable = $usercancreate;
print $form->editfieldkey("AvailabilityPeriod", 'availability', '', $object, $editenable);
print ' ';
if ($action == 'editavailability') {
@@ -2310,7 +2312,7 @@ if ($action == 'create' && $user->rights->commande->creer)
// Source reason (why we have an ordrer)
print ' ';
- $editenable = $user->rights->commande->creer;
+ $editenable = $usercancreate;
print $form->editfieldkey("Channel", 'demandreason', '', $object, $editenable);
print ' ';
if ($action == 'editdemandreason') {
@@ -2323,7 +2325,7 @@ if ($action == 'create' && $user->rights->commande->creer)
// TODO Order mode (how we receive order). Not yet implemented
/*
print ' ';
- $editenable = $user->rights->commande->creer;
+ $editenable = $usercancreate;
print $form->editfieldkey("SourceMode", 'inputmode', '', $object, $editenable);
print ' ';
if ($action == 'editinputmode') {
@@ -2355,7 +2357,7 @@ if ($action == 'create' && $user->rights->commande->creer)
// Incoterms
if (!empty($conf->incoterm->enabled)) {
print ' ';
- $editenable = $user->rights->commande->creer;
+ $editenable = $usercancreate;
print $form->editfieldkey("IncotermLabel", 'incoterm', '', $object, $editenable);
print ' ';
print '';
@@ -2373,7 +2375,7 @@ if ($action == 'create' && $user->rights->commande->creer)
// Bank Account
if (! empty($conf->global->BANK_ASK_PAYMENT_BANK_DURING_ORDER) && ! empty($conf->banque->enabled)) {
print ' ';
- $editenable = $user->rights->commande->creer;
+ $editenable = $usercancreate;
print $form->editfieldkey("BankAccount", 'bankaccount', '', $object, $editenable);
print ' ';
if ($action == 'editbankaccount') {
@@ -2497,7 +2499,7 @@ if ($action == 'create' && $user->rights->commande->creer)
/*
* Form to add new line
*/
- if ($object->statut == Commande::STATUS_DRAFT && $user->rights->commande->creer && $action != 'selectlines')
+ if ($object->statut == Commande::STATUS_DRAFT && $usercancreate && $action != 'selectlines')
{
if ($action != 'editline')
{
@@ -2528,22 +2530,19 @@ if ($action == 'create' && $user->rights->commande->creer)
if (empty($reshook)) {
// Send
if ($object->statut > Commande::STATUS_DRAFT) {
- if ((empty($conf->global->MAIN_USE_ADVANCED_PERMS) || $user->rights->commande->order_advance->send)) {
+ if ($usercansend) {
print '';
} else
print '';
}
// Valid
- if ($object->statut == Commande::STATUS_DRAFT && $object->total_ttc >= 0 && $numlines > 0 &&
- ((empty($conf->global->MAIN_USE_ADVANCED_PERMS) && ! empty($user->rights->commande->creer))
- || (! empty($conf->global->MAIN_USE_ADVANCED_PERMS) && ! empty($user->rights->commande->order_advance->validate)))
- )
+ if ($object->statut == Commande::STATUS_DRAFT && $object->total_ttc >= 0 && $numlines > 0 && $usercanvalidate)
{
print '';
}
// Edit
- if ($object->statut == Commande::STATUS_VALIDATED && $user->rights->commande->creer) {
+ if ($object->statut == Commande::STATUS_VALIDATED && $usercancreate) {
print '';
}
// Create event
@@ -2597,12 +2596,12 @@ if ($action == 'create' && $user->rights->commande->creer)
}
// Reopen a closed order
- if (($object->statut == Commande::STATUS_CLOSED || $object->statut == Commande::STATUS_CANCELED) && $user->rights->commande->creer) {
+ if (($object->statut == Commande::STATUS_CLOSED || $object->statut == Commande::STATUS_CANCELED) && $usercancreate) {
print '';
}
// Set to shipped
- if (($object->statut == Commande::STATUS_VALIDATED || $object->statut == Commande::STATUS_SHIPMENTONPROCESS) && $user->rights->commande->cloturer) {
+ if (($object->statut == Commande::STATUS_VALIDATED || $object->statut == Commande::STATUS_SHIPMENTONPROCESS) && $usercanclose) {
print '';
}
@@ -2612,31 +2611,28 @@ if ($action == 'create' && $user->rights->commande->creer)
if (! empty($conf->facture->enabled) && $user->rights->facture->creer && empty($conf->global->WORKFLOW_DISABLE_CREATE_INVOICE_FROM_ORDER)) {
print '';
}
- if ($user->rights->commande->creer && $object->statut >= Commande::STATUS_VALIDATED && empty($conf->global->WORKFLOW_DISABLE_CLASSIFY_BILLED_FROM_ORDER) && empty($conf->global->WORKFLOW_BILL_ON_SHIPMENT)) {
+ if ($usercancreate && $object->statut >= Commande::STATUS_VALIDATED && empty($conf->global->WORKFLOW_DISABLE_CLASSIFY_BILLED_FROM_ORDER) && empty($conf->global->WORKFLOW_BILL_ON_SHIPMENT)) {
print '';
}
}
if ($object->statut > Commande::STATUS_DRAFT && $object->billed) {
- if ($user->rights->commande->creer && $object->statut >= Commande::STATUS_VALIDATED && empty($conf->global->WORKFLOW_DISABLE_CLASSIFY_BILLED_FROM_ORDER) && empty($conf->global->WORKFLOW_BILL_ON_SHIPMENT)) {
+ if ($usercancreate && $object->statut >= Commande::STATUS_VALIDATED && empty($conf->global->WORKFLOW_DISABLE_CLASSIFY_BILLED_FROM_ORDER) && empty($conf->global->WORKFLOW_BILL_ON_SHIPMENT)) {
print '';
}
}
// Clone
- if ($user->rights->commande->creer) {
+ if ($usercancreate) {
print '';
}
// Cancel order
- if ($object->statut == Commande::STATUS_VALIDATED &&
- ((empty($conf->global->MAIN_USE_ADVANCED_PERMS) && ! empty($user->rights->commande->cloturer))
- || (! empty($conf->global->MAIN_USE_ADVANCED_PERMS) && ! empty($user->rights->commande->order_advance->annuler)))
- )
+ if ($object->statut == Commande::STATUS_VALIDATED && (! empty($usercanclose) || ! empty($usercancancel)))
{
print '';
}
// Delete order
- if ($user->rights->commande->supprimer) {
+ if ($usercandelete) {
if ($numshipping == 0) {
print '';
} else {
@@ -2659,18 +2655,18 @@ if ($action == 'create' && $user->rights->commande->creer)
// Documents
$comref = dol_sanitizeFileName($object->ref);
$relativepath = $comref . '/' . $comref . '.pdf';
- $filedir = $conf->commande->dir_output . '/' . $comref;
+ $filedir = $conf->commande->multidir_output[$object->entity] . '/' . $comref;
$urlsource = $_SERVER["PHP_SELF"] . "?id=" . $object->id;
- $genallowed = $user->rights->commande->lire;
- $delallowed = $user->rights->commande->creer;
- print $formfile->showdocuments('commande', $comref, $filedir, $urlsource, $genallowed, $delallowed, $object->modelpdf, 1, 0, 0, 28, 0, '', '', '', $soc->default_lang);
+ $genallowed = $usercanread;
+ $delallowed = $usercancreate;
+ print $formfile->showdocuments('commande', $comref, $filedir, $urlsource, $genallowed, $delallowed, $object->modelpdf, 1, 0, 0, 28, 0, '', '', '', $soc->default_lang, '', $object);
// Show links to link elements
$linktoelem = $form->showLinkToObjectBlock($object, null, array('order'));
$compatibleImportElementsList = false;
- if($user->rights->commande->creer
+ if($usercancreate
&& $object->statut == Commande::STATUS_DRAFT)
{
$compatibleImportElementsList = array('commande','propal'); // import from linked elements
@@ -2707,7 +2703,7 @@ if ($action == 'create' && $user->rights->commande->creer)
// Presend form
$modelmail='order_send';
$defaulttopic='SendOrderRef';
- $diroutput = $conf->commande->dir_output;
+ $diroutput = $conf->commande->multidir_output[$object->entity];
$trackid = 'ord'.$object->id;
include DOL_DOCUMENT_ROOT.'/core/tpl/card_presend.tpl.php';
diff --git a/htdocs/commande/class/commande.class.php b/htdocs/commande/class/commande.class.php
index fb246c7e232..f9662a562f2 100644
--- a/htdocs/commande/class/commande.class.php
+++ b/htdocs/commande/class/commande.class.php
@@ -433,8 +433,8 @@ class Commande extends CommonOrder
// We rename directory ($this->ref = old ref, $num = new ref) in order not to lose the attachments
$oldref = dol_sanitizeFileName($this->ref);
$newref = dol_sanitizeFileName($num);
- $dirsource = $conf->commande->dir_output.'/'.$oldref;
- $dirdest = $conf->commande->dir_output.'/'.$newref;
+ $dirsource = $conf->commande->multidir_output[$this->entity].'/'.$oldref;
+ $dirdest = $conf->commande->multidir_output[$this->entity].'/'.$newref;
if (! $error && file_exists($dirsource))
{
dol_syslog(get_class($this)."::valid() rename dir ".$dirsource." into ".$dirdest);
@@ -443,7 +443,7 @@ class Commande extends CommonOrder
{
dol_syslog("Rename ok");
// Rename docs starting with $oldref with $newref
- $listoffiles=dol_dir_list($conf->commande->dir_output.'/'.$newref, 'files', 1, '^'.preg_quote($oldref, '/'));
+ $listoffiles=dol_dir_list($conf->commande->multidir_output[$this->entity].'/'.$newref, 'files', 1, '^'.preg_quote($oldref, '/'));
foreach($listoffiles as $fileentry)
{
$dirsource=$fileentry['name'];
@@ -865,7 +865,7 @@ class Commande extends CommonOrder
$sql.= ", ".($this->remise_percent>0?$this->db->escape($this->remise_percent):0);
$sql.= ", ".(int) $this->fk_incoterms;
$sql.= ", '".$this->db->escape($this->location_incoterms)."'";
- $sql.= ", ".$conf->entity;
+ $sql.= ", ".setEntity($this);
$sql.= ", ".($this->module_source ? "'".$this->db->escape($this->module_source)."'" : "null");
$sql.= ", ".($this->pos_source != '' ? "'".$this->db->escape($this->pos_source)."'" : "null");
$sql.= ", ".(int) $this->fk_multicurrency;
@@ -1238,6 +1238,7 @@ class Commande extends CommonOrder
$this->lines[$i] = $line;
}
+ $this->entity = $object->entity;
$this->socid = $object->socid;
$this->fk_project = $object->fk_project;
$this->cond_reglement_id = $object->cond_reglement_id;
@@ -1457,11 +1458,11 @@ class Commande extends CommonOrder
$pu_ht_devise = $tabprice[19];
// Rang to use
- $rangtouse = $rang;
- if ($rangtouse == -1)
+ $ranktouse = $rang;
+ if ($ranktouse == -1)
{
$rangmax = $this->line_max($fk_parent_line);
- $rangtouse = $rangmax + 1;
+ $ranktouse = $rangmax + 1;
}
// TODO A virer
@@ -1495,7 +1496,7 @@ class Commande extends CommonOrder
$this->line->fk_remise_except=$fk_remise_except;
$this->line->remise_percent=$remise_percent;
$this->line->subprice=$pu_ht;
- $this->line->rang=$rangtouse;
+ $this->line->rang=$ranktouse;
$this->line->info_bits=$info_bits;
$this->line->total_ht=$total_ht;
$this->line->total_tva=$total_tva;
@@ -3335,10 +3336,10 @@ class Commande extends CommonOrder
{
// Remove directory with files
$comref = dol_sanitizeFileName($this->ref);
- if ($conf->commande->dir_output && !empty($this->ref))
+ if ($conf->commande->multidir_output[$this->entity] && !empty($this->ref))
{
- $dir = $conf->commande->dir_output . "/" . $comref ;
- $file = $conf->commande->dir_output . "/" . $comref . "/" . $comref . ".pdf";
+ $dir = $conf->commande->multidir_output[$this->entity] . "/" . $comref ;
+ $file = $conf->commande->multidir_output[$this->entity] . "/" . $comref . "/" . $comref . ".pdf";
if (file_exists($file)) // We must delete all files before deleting directory
{
dol_delete_preview($this);
diff --git a/htdocs/commande/document.php b/htdocs/commande/document.php
index 37cbc70cd40..ec6ed2fd91b 100644
--- a/htdocs/commande/document.php
+++ b/htdocs/commande/document.php
@@ -72,7 +72,7 @@ $object = new Commande($db);
if ($object->fetch($id))
{
$object->fetch_thirdparty();
- $upload_dir = $conf->commande->dir_output . "/" . dol_sanitizeFileName($object->ref);
+ $upload_dir = $conf->commande->multidir_output[$object->entity] . "/" . dol_sanitizeFileName($object->ref);
}
include_once DOL_DOCUMENT_ROOT . '/core/actions_linkedfiles.inc.php';
@@ -92,7 +92,7 @@ if ($id > 0 || ! empty($ref))
{
$object->fetch_thirdparty();
- $upload_dir = $conf->commande->dir_output.'/'.dol_sanitizeFileName($object->ref);
+ $upload_dir = $conf->commande->multidir_output[$object->entity].'/'.dol_sanitizeFileName($object->ref);
$head = commande_prepare_head($object);
dol_fiche_head($head, 'documents', $langs->trans('CustomerOrder'), -1, 'order');
@@ -174,7 +174,7 @@ if ($id > 0 || ! empty($ref))
$modulepart = 'commande';
$permission = $user->rights->commande->creer;
$permtoedit = $user->rights->commande->creer;
- $param = '&id=' . $object->id;
+ $param = '&id=' . $object->id.'&entity=' . (! empty($object->entity)?$object->entity:$conf->entity);
include_once DOL_DOCUMENT_ROOT . '/core/tpl/document_actions_post_headers.tpl.php';
}
else
diff --git a/htdocs/commande/index.php b/htdocs/commande/index.php
index a0e87bf63a3..160b6f70627 100644
--- a/htdocs/commande/index.php
+++ b/htdocs/commande/index.php
@@ -241,7 +241,7 @@ $max=5;
* Last modified orders
*/
-$sql = "SELECT c.rowid, c.ref, c.fk_statut, c.facture, c.date_cloture as datec, c.tms as datem,";
+$sql = "SELECT c.rowid, c.entity, c.ref, c.fk_statut, c.facture, c.date_cloture as datec, c.tms as datem,";
$sql.= " s.nom as name, s.rowid as socid";
$sql.= ", s.client";
$sql.= ", s.code_client";
@@ -297,7 +297,7 @@ if ($resql)
print ' ';
$filename=dol_sanitizeFileName($obj->ref);
- $filedir=$conf->commande->dir_output . '/' . dol_sanitizeFileName($obj->ref);
+ $filedir=$conf->commande->multidir_output[$obj->entity] . '/' . dol_sanitizeFileName($obj->ref);
$urlsource=$_SERVER['PHP_SELF'].'?id='.$obj->rowid;
print $formfile->getDocumentsLink($commandestatic->element, $filename, $filedir);
print ' ';
@@ -323,7 +323,7 @@ else dol_print_error($db);
*/
if (! empty($conf->commande->enabled))
{
- $sql = "SELECT c.rowid, c.ref, c.fk_statut, c.facture, s.nom as name, s.rowid as socid";
+ $sql = "SELECT c.rowid, c.entity, c.ref, c.fk_statut, c.facture, s.nom as name, s.rowid as socid";
$sql.= ", s.client";
$sql.= ", s.code_client";
$sql.= ", s.canvas";
@@ -377,7 +377,7 @@ if (! empty($conf->commande->enabled))
print '
';
$filename=dol_sanitizeFileName($obj->ref);
- $filedir=$conf->commande->dir_output . '/' . dol_sanitizeFileName($obj->ref);
+ $filedir=$conf->commande->multidir_output[$obj->entity] . '/' . dol_sanitizeFileName($obj->ref);
$urlsource=$_SERVER['PHP_SELF'].'?id='.$obj->rowid;
print $formfile->getDocumentsLink($commandestatic->element, $filename, $filedir);
print ' ';
@@ -405,7 +405,7 @@ if (! empty($conf->commande->enabled))
*/
if (! empty($conf->commande->enabled))
{
- $sql = "SELECT c.rowid, c.ref, c.fk_statut, c.facture, s.nom as name, s.rowid as socid";
+ $sql = "SELECT c.rowid, c.entity, c.ref, c.fk_statut, c.facture, s.nom as name, s.rowid as socid";
$sql.= ", s.client";
$sql.= ", s.code_client";
$sql.= ", s.canvas";
@@ -459,7 +459,7 @@ if (! empty($conf->commande->enabled))
print '
';
$filename=dol_sanitizeFileName($obj->ref);
- $filedir=$conf->commande->dir_output . '/' . dol_sanitizeFileName($obj->ref);
+ $filedir=$conf->commande->multidir_output[$obj->entity] . '/' . dol_sanitizeFileName($obj->ref);
$urlsource=$_SERVER['PHP_SELF'].'?id='.$obj->rowid;
print $formfile->getDocumentsLink($commandestatic->element, $filename, $filedir);
print ' ';
diff --git a/htdocs/commande/list.php b/htdocs/commande/list.php
index f64aadf0e53..66a3862bd2c 100644
--- a/htdocs/commande/list.php
+++ b/htdocs/commande/list.php
@@ -91,7 +91,7 @@ $id = (GETPOST('orderid')?GETPOST('orderid', 'int'):GETPOST('id', 'int'));
if ($user->societe_id) $socid=$user->societe_id;
$result = restrictedArea($user, 'commande', $id, '');
-$diroutputmassaction=$conf->commande->dir_output . '/temp/massgeneration/'.$user->id;
+$diroutputmassaction=$conf->commande->multidir_output[$conf->entity] . '/temp/massgeneration/'.$user->id;
// Load variable for pagination
$limit = GETPOST('limit', 'int')?GETPOST('limit', 'int'):$conf->liste_limit;
@@ -143,6 +143,7 @@ $arrayfields=array(
'c.total_ttc'=>array('label'=>"AmountTTC", 'checked'=>0),
'c.datec'=>array('label'=>"DateCreation", 'checked'=>0, 'position'=>500),
'c.tms'=>array('label'=>"DateModificationShort", 'checked'=>0, 'position'=>500),
+ 'c.date_cloture'=>array('label'=>"DateClosing", 'checked'=>0, 'position'=>500),
'c.fk_statut'=>array('label'=>"Status", 'checked'=>1, 'position'=>1000),
'c.facture'=>array('label'=>"Billed", 'checked'=>1, 'position'=>1000, 'enabled'=>(empty($conf->global->WORKFLOW_BILL_ON_SHIPMENT)))
);
@@ -217,7 +218,7 @@ if (empty($reshook))
$objectlabel='Orders';
$permtoread = $user->rights->commande->lire;
$permtodelete = $user->rights->commande->supprimer;
- $uploaddir = $conf->commande->dir_output;
+ $uploaddir = $conf->commande->multidir_output[$conf->entity];
$trigger_name='ORDER_SENTBYMAIL';
include DOL_DOCUMENT_ROOT.'/core/actions_massactions.inc.php';
}
@@ -247,7 +248,7 @@ $sql.= " typent.code as typent_code,";
$sql.= " state.code_departement as state_code, state.nom as state_name,";
$sql.= ' c.rowid, c.ref, c.total_ht, c.tva as total_tva, c.total_ttc, c.ref_client,';
$sql.= ' c.date_valid, c.date_commande, c.note_private, c.date_livraison as date_delivery, c.fk_statut, c.facture as billed,';
-$sql.= ' c.date_creation as date_creation, c.tms as date_update,';
+$sql.= ' c.date_creation as date_creation, c.tms as date_update, c.date_cloture as date_cloture,';
$sql.= " p.rowid as project_id, p.ref as project_ref, p.title as project_label";
if ($search_categ_cus) $sql .= ", cc.fk_categorie, cc.fk_soc";
// Add fields from extrafields
@@ -693,6 +694,12 @@ if ($resql)
print '
';
print ' ';
}
+ // Date cloture
+ if (! empty($arrayfields['c.date_cloture']['checked']))
+ {
+ print '
';
+ print ' ';
+ }
// Status
if (! empty($arrayfields['c.fk_statut']['checked']))
{
@@ -748,7 +755,8 @@ if ($resql)
print $hookmanager->resPrint;
if (! empty($arrayfields['c.datec']['checked'])) print_liste_field_titre($arrayfields['c.datec']['label'], $_SERVER["PHP_SELF"], "c.date_creation", "", $param, '', $sortfield, $sortorder, 'center nowrap ');
if (! empty($arrayfields['c.tms']['checked'])) print_liste_field_titre($arrayfields['c.tms']['label'], $_SERVER["PHP_SELF"], "c.tms", "", $param, '', $sortfield, $sortorder, 'center nowrap ');
- if (! empty($arrayfields['c.fk_statut']['checked'])) print_liste_field_titre($arrayfields['c.fk_statut']['label'], $_SERVER["PHP_SELF"], "c.fk_statut", "", $param, '', $sortfield, $sortorder, 'right ');
+ if (! empty($arrayfields['c.date_cloture']['checked'])) print_liste_field_titre($arrayfields['c.date_cloture']['label'], $_SERVER["PHP_SELF"], "c.date_cloture", "", $param, '', $sortfield, $sortorder, 'center nowrap ');
+ if (! empty($arrayfields['c.fk_statut']['checked'])) print_liste_field_titre($arrayfields['c.fk_statut']['label'], $_SERVER["PHP_SELF"], "c.fk_statut", "", $param, '', $sortfield, $sortorder, 'right ');
if (! empty($arrayfields['c.facture']['checked'])) print_liste_field_titre($arrayfields['c.facture']['label'], $_SERVER["PHP_SELF"], 'c.facture', '', $param, '', $sortfield, $sortorder, 'center ');
print_liste_field_titre($selectedfields, $_SERVER["PHP_SELF"], "", '', $param, '', $sortfield, $sortorder, 'maxwidthsearch center ');
print ''."\n";
@@ -926,7 +934,7 @@ if ($resql)
print '
';
$filename=dol_sanitizeFileName($obj->ref);
- $filedir=$conf->commande->dir_output . '/' . dol_sanitizeFileName($obj->ref);
+ $filedir=$conf->commande->multidir_output[$conf->entity] . '/' . dol_sanitizeFileName($obj->ref);
$urlsource=$_SERVER['PHP_SELF'].'?id='.$obj->rowid;
print $formfile->getDocumentsLink($generic_commande->element, $filename, $filedir);
print ' ';
@@ -1073,7 +1081,7 @@ if ($resql)
// Extra fields
include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_print_fields.tpl.php';
// Fields from hook
- $parameters=array('arrayfields'=>$arrayfields, 'obj'=>$obj);
+ $parameters=array('arrayfields'=>$arrayfields, 'obj'=>$obj, 'i'=>$i);
$reshook=$hookmanager->executeHooks('printFieldListValue', $parameters); // Note that $action and $object may have been modified by hook
print $hookmanager->resPrint;
// Date creation
@@ -1092,6 +1100,14 @@ if ($resql)
print '';
if (! $i) $totalarray['nbfield']++;
}
+ // Date cloture
+ if (! empty($arrayfields['c.date_cloture']['checked']))
+ {
+ print '
';
+ print dol_print_date($db->jdate($obj->date_cloture), 'dayhour', 'tzuser');
+ print ' ';
+ if (! $i) $totalarray['nbfield']++;
+ }
// Status
if (! empty($arrayfields['c.fk_statut']['checked']))
{
diff --git a/htdocs/commande/orderstoinvoice.php b/htdocs/commande/orderstoinvoice.php
index 33acc2b4969..fa983e46f8e 100644
--- a/htdocs/commande/orderstoinvoice.php
+++ b/htdocs/commande/orderstoinvoice.php
@@ -548,7 +548,7 @@ if (($action != 'create' && $action != 'add') || ($action == 'create' && $error)
';
$filename=dol_sanitizeFileName($objp->ref);
- $filedir=$conf->commande->dir_output . '/' . dol_sanitizeFileName($objp->ref);
+ $filedir=$conf->commande->multidir_output[$objp->entity] . '/' . dol_sanitizeFileName($objp->ref);
$urlsource=$_SERVER['PHP_SELF'].'?id='.$objp->rowid;
print $formfile->getDocumentsLink($generic_commande->element, $filename, $filedir);
print '';
diff --git a/htdocs/compta/bank/class/api_bankaccounts.class.php b/htdocs/compta/bank/class/api_bankaccounts.class.php
index 72ba4c82db1..ba21b6c09c1 100644
--- a/htdocs/compta/bank/class/api_bankaccounts.class.php
+++ b/htdocs/compta/bank/class/api_bankaccounts.class.php
@@ -23,6 +23,7 @@ require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
/**
* API class for accounts
*
+ * @property DoliDB db
* @access protected
* @class DolibarrApiAccess {@requires user,external}
*/
@@ -166,6 +167,142 @@ class BankAccounts extends DolibarrApi
return $account->id;
}
+ /**
+ * Create an internal wire transfer between two bank accounts
+ *
+ * @param int $bankaccount_from_id BankAccount ID to use as the source of the internal wire transfer {@from body}{@required true}
+ * @param int $bankaccount_to_id BankAccount ID to use as the destination of the internal wire transfer {@from body}{@required true}
+ * @param string $date Date of the internal wire transfer (UNIX timestamp) {@from body}{@required true}{@type timestamp}
+ * @param string $description Description of the internal wire transfer {@from body}{@required true}
+ * @param float $amount Amount to transfer from the source to the destination BankAccount {@from body}{@required true}
+ * @param float $amount_to Amount to transfer to the destination BankAccount (only when accounts does not share the same currency) {@from body}{@required false}
+ *
+ * @url POST /transfer
+ *
+ * @return array
+ *
+ * @status 201
+ *
+ * @throws 401 Unauthorized: User does not have permission to configure bank accounts
+ * @throws 404 Not Found: Either the source or the destination bankaccount for the provided id does not exist
+ * @throws 422 Unprocessable Entity: Refer to detailed exception message for the cause
+ * @throws 500 Internal Server Error: Error(s) returned by the RDBMS
+ */
+ public function transfer($bankaccount_from_id = 0, $bankaccount_to_id = 0, $date = null, $description = "", $amount = 0.0, $amount_to = 0.0)
+ {
+ if (! DolibarrApiAccess::$user->rights->banque->configurer) {
+ throw new RestException(401);
+ }
+
+ if ($bankaccount_from_id === $bankaccount_to_id) {
+ throw new RestException(422, 'bankaccount_from_id and bankaccount_to_id must be different !');
+ }
+
+ require_once DOL_DOCUMENT_ROOT.'/compta/bank/class/account.class.php';
+
+ $accountfrom = new Account($this->db);
+ $resultAccountFrom = $accountfrom->fetch($bankaccount_from_id);
+
+ if ($resultAccountFrom === 0) {
+ throw new RestException(404, 'The BankAccount for bankaccount_from_id provided does not exist.');
+ }
+
+ $accountto = new Account($this->db);
+ $resultAccountTo = $accountto->fetch($bankaccount_to_id);
+
+ if ($resultAccountTo === 0) {
+ throw new RestException(404, 'The BankAccount for bankaccount_to_id provided does not exist.');
+ }
+
+ if ($accountto->currency_code == $accountfrom->currency_code)
+ {
+ $amount_to = $amount;
+ }
+ else
+ {
+ if (!$amount_to || empty($amount_to))
+ {
+ throw new RestException(422, 'You must provide amount_to value since bankaccount_from and bankaccount_to does not share the same currency.');
+ }
+ }
+
+ $this->db->begin();
+
+ $error = 0;
+ $bank_line_id_from = 0;
+ $bank_line_id_to = 0;
+ $result = 0;
+ $user = DolibarrApiAccess::$user;
+
+ // By default, electronic transfert from bank to bank
+ $typefrom='PRE';
+ $typeto='VIR';
+
+ if ($accountto->courant == Account::TYPE_CASH || $accountfrom->courant == Account::TYPE_CASH)
+ {
+ // This is transfer of change
+ $typefrom='LIQ';
+ $typeto='LIQ';
+ }
+
+ /**
+ * Creating bank line records
+ */
+
+ if (!$error) {
+ $bank_line_id_from = $accountfrom->addline($date, $typefrom, $description, -1*price2num($amount), '', '', $user);
+ }
+ if (!($bank_line_id_from > 0)) {
+ $error++;
+ }
+
+ if (!$error) {
+ $bank_line_id_to = $accountto->addline($date, $typeto, $description, price2num($amount_to), '', '', $user);
+ }
+ if (!($bank_line_id_to > 0)) {
+ $error++;
+ }
+
+ /**
+ * Creating links between bank line record and its source
+ */
+
+ $url = DOL_URL_ROOT.'/compta/bank/line.php?rowid=';
+ $label = '(banktransfert)';
+ $type = 'banktransfert';
+
+ if (!$error) {
+ $result = $accountfrom->add_url_line($bank_line_id_from, $bank_line_id_to, $url, $label, $type);
+ }
+ if (!($result > 0)) {
+ $error++;
+ }
+
+ if (!$error) {
+ $result = $accountto->add_url_line($bank_line_id_to, $bank_line_id_from, $url, $label, $type);
+ }
+ if (!($result > 0)) {
+ $error++;
+ }
+
+ if (!$error)
+ {
+ $this->db->commit();
+
+ return array(
+ 'success' => array(
+ 'code' => 201,
+ 'message' => 'Internal wire transfer created successfully.'
+ )
+ );
+ }
+ else
+ {
+ $this->db->rollback();
+ throw new RestException(500, $accountfrom->error.' '.$accountto->error);
+ }
+ }
+
/**
* Update account
*
diff --git a/htdocs/compta/bank/index.html b/htdocs/compta/bank/index.html
new file mode 100644
index 00000000000..8b137891791
--- /dev/null
+++ b/htdocs/compta/bank/index.html
@@ -0,0 +1 @@
+
diff --git a/htdocs/compta/bank/treso.php b/htdocs/compta/bank/treso.php
index f17e622210e..f9231f4f247 100644
--- a/htdocs/compta/bank/treso.php
+++ b/htdocs/compta/bank/treso.php
@@ -267,9 +267,9 @@ if ($_REQUEST["account"] || $_REQUEST["ref"])
$parameters = array('obj' => $obj);
$reshook = $hookmanager->executeHooks('moreFamily', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
if(empty($reshook)){
- $ref = isset($hookmanager->resArray['ref']) ? $hookmanager->resArray['ref'] : '';
- $refcomp = isset($hookmanager->resArray['refcomp']) ? $hookmanager->resArray['refcomp'] : '';
- $paiement = isset($hookmanager->resArray['paiement']) ? $hookmanager->resArray['paiement'] : 0;
+ $ref = isset($hookmanager->resArray['ref']) ? $hookmanager->resArray['ref'] : $ref;
+ $refcomp = isset($hookmanager->resArray['refcomp']) ? $hookmanager->resArray['refcomp'] : $refcomp;
+ $paiement = isset($hookmanager->resArray['paiement']) ? $hookmanager->resArray['paiement'] : $paiement;
}
$total_ttc = $obj->total_ttc;
diff --git a/htdocs/compta/facture/card.php b/htdocs/compta/facture/card.php
index bfa21a9c759..9c4a10a19ad 100644
--- a/htdocs/compta/facture/card.php
+++ b/htdocs/compta/facture/card.php
@@ -945,7 +945,6 @@ if (empty($reshook))
$object->location_incoterms = GETPOST('location_incoterms', 'alpha');
$object->multicurrency_code = GETPOST('multicurrency_code', 'alpha');
$object->multicurrency_tx = GETPOST('originmulticurrency_tx', 'int');
- $object->entity = (GETPOSTISSET('entity')?GETPOST('entity', 'int'):$conf->entity);
// Proprietes particulieres a facture de remplacement
$object->fk_facture_source = $_POST['fac_replacement'];
@@ -979,7 +978,7 @@ if (empty($reshook))
if (! $error)
{
- if(!empty($originentity)){
+ if (!empty($originentity)) {
$object->entity = $originentity;
}
$object->socid = GETPOST('socid', 'int');
@@ -1001,7 +1000,6 @@ if (empty($reshook))
$object->location_incoterms = GETPOST('location_incoterms', 'alpha');
$object->multicurrency_code = GETPOST('multicurrency_code', 'alpha');
$object->multicurrency_tx = GETPOST('originmulticurrency_tx', 'int');
- $object->entity = (GETPOSTISSET('entity')?GETPOST('entity', 'int'):$conf->entity);
// Proprietes particulieres a facture avoir
$object->fk_facture_source = $sourceinvoice > 0 ? $sourceinvoice : '';
@@ -1183,7 +1181,6 @@ if (empty($reshook))
$object->location_incoterms = GETPOST('location_incoterms', 'alpha');
$object->multicurrency_code = GETPOST('multicurrency_code', 'alpha');
$object->multicurrency_tx = GETPOST('originmulticurrency_tx', 'int');
- $object->entity = (GETPOSTISSET('entity')?GETPOST('entity', 'int'):$conf->entity);
// Source facture
$object->fac_rec = GETPOST('fac_rec', 'int');
@@ -1234,7 +1231,6 @@ if (empty($reshook))
$object->location_incoterms = GETPOST('location_incoterms', 'alpha');
$object->multicurrency_code = GETPOST('multicurrency_code', 'alpha');
$object->multicurrency_tx = GETPOST('originmulticurrency_tx', 'int');
- $object->entity = (GETPOSTISSET('entity')?GETPOST('entity', 'int'):$conf->entity);
if (GETPOST('type') == Facture::TYPE_SITUATION)
{
@@ -2491,7 +2487,7 @@ if (empty($reshook))
include DOL_DOCUMENT_ROOT.'/core/actions_sendmails.inc.php';
// Actions to build doc
- $upload_dir = $conf->facture->dir_output;
+ $upload_dir = $conf->facture->multidir_output[$object->entity];
$permissioncreate=$usercancreate;
include DOL_DOCUMENT_ROOT.'/core/actions_builddoc.inc.php';
@@ -5136,12 +5132,12 @@ elseif ($id > 0 || ! empty($ref))
// Documents generes
$filename = dol_sanitizeFileName($object->ref);
- $filedir = $conf->facture->dir_output . '/' . dol_sanitizeFileName($object->ref);
+ $filedir = $conf->facture->multidir_output[$object->entity] . '/' . dol_sanitizeFileName($object->ref);
$urlsource = $_SERVER['PHP_SELF'] . '?facid=' . $object->id;
$genallowed = $usercanread;
$delallowed = $usercancreate;
- print $formfile->showdocuments('facture', $filename, $filedir, $urlsource, $genallowed, $delallowed, $object->modelpdf, 1, 0, 0, 28, 0, '', '', '', $soc->default_lang);
+ print $formfile->showdocuments('facture', $filename, $filedir, $urlsource, $genallowed, $delallowed, $object->modelpdf, 1, 0, 0, 28, 0, '', '', '', $soc->default_lang, '', $object);
$somethingshown = $formfile->numoffiles;
// Show links to link elements
@@ -5188,7 +5184,7 @@ elseif ($id > 0 || ! empty($ref))
// Presend form
$modelmail='facture_send';
$defaulttopic='SendBillRef';
- $diroutput = $conf->facture->dir_output;
+ $diroutput = $conf->facture->multidir_output[$object->entity];
$trackid = 'inv'.$object->id;
include DOL_DOCUMENT_ROOT.'/core/tpl/card_presend.tpl.php';
diff --git a/htdocs/compta/facture/class/facture.class.php b/htdocs/compta/facture/class/facture.class.php
index 763eea6cdd2..33de9d47e18 100644
--- a/htdocs/compta/facture/class/facture.class.php
+++ b/htdocs/compta/facture/class/facture.class.php
@@ -318,7 +318,6 @@ class Facture extends CommonInvoice
if (! $this->cond_reglement_id) $this->cond_reglement_id = 0;
if (! $this->mode_reglement_id) $this->mode_reglement_id = 0;
$this->brouillon = 1;
- if (empty($this->entity)) $this->entity = $conf->entity;
// Multicurrency (test on $this->multicurrency_tx because we should take the default rate only if not using origin rate)
if (!empty($this->multicurrency_code) && empty($this->multicurrency_tx)) list($this->fk_multicurrency,$this->multicurrency_tx) = MultiCurrency::getIdAndTxFromCode($this->db, $this->multicurrency_code);
@@ -493,7 +492,7 @@ class Facture extends CommonInvoice
$sql.= ")";
$sql.= " VALUES (";
$sql.= "'(PROV)'";
- $sql.= ", ".$this->entity;
+ $sql.= ", ".setEntity($this);
$sql.= ", ".($this->ref_ext?"'".$this->db->escape($this->ref_ext)."'":"null");
$sql.= ", '".$this->db->escape($this->type)."'";
$sql.= ", '".$socid."'";
@@ -936,8 +935,8 @@ class Facture extends CommonInvoice
$facture->remise_absolue = $this->remise_absolue;
$facture->remise_percent = $this->remise_percent;
- $facture->origin = $this->origin;
- $facture->origin_id = $this->origin_id;
+ $facture->origin = $this->origin;
+ $facture->origin_id = $this->origin_id;
$facture->lines = $this->lines; // Array of lines of invoice
$facture->products = $this->lines; // Tant que products encore utilise
@@ -1624,7 +1623,11 @@ class Facture extends CommonInvoice
$this->tab_previous_situation_invoice = array();
$this->tab_next_situation_invoice = array();
- $sql = 'SELECT rowid, situation_counter FROM '.MAIN_DB_PREFIX.'facture WHERE rowid <> '.$this->id.' AND entity = '.$conf->entity.' AND situation_cycle_ref = '.(int) $this->situation_cycle_ref.' ORDER BY situation_counter ASC';
+ $sql = 'SELECT rowid, situation_counter FROM '.MAIN_DB_PREFIX.'facture';
+ $sql.= ' WHERE rowid <> '.$this->id;
+ $sql.= ' AND entity = '.$this->entity;
+ $sql.= ' AND situation_cycle_ref = '.(int) $this->situation_cycle_ref;
+ $sql.= ' ORDER BY situation_counter ASC';
dol_syslog(get_class($this).'::fetchPreviousNextSituationInvoice ', LOG_DEBUG);
$result = $this->db->query($sql);
@@ -2825,11 +2828,11 @@ class Facture extends CommonInvoice
$pu_ht_devise = $tabprice[19];
// Rank to use
- $rangtouse = $rang;
- if ($rangtouse == -1)
+ $ranktouse = $rang;
+ if ($ranktouse == -1)
{
$rangmax = $this->line_max($fk_parent_line);
- $rangtouse = $rangmax + 1;
+ $ranktouse = $rangmax + 1;
}
// Insert line
@@ -2863,7 +2866,7 @@ class Facture extends CommonInvoice
$this->line->date_start=$date_start;
$this->line->date_end=$date_end;
$this->line->ventil=$ventil;
- $this->line->rang=$rangtouse;
+ $this->line->rang=$ranktouse;
$this->line->info_bits=$info_bits;
$this->line->fk_remise_except=$fk_remise_except;
@@ -3700,11 +3703,11 @@ class Facture extends CommonInvoice
$sql.= " AND ff.type IS NULL"; // Renvoi vrai si pas facture de remplacement
$sql.= " AND f.type != ".self::TYPE_CREDIT_NOTE; // Type non 2 si facture non avoir
- if($conf->global->INVOICE_USE_SITUATION_CREDIT_NOTE){
+ if (! empty($conf->global->INVOICE_USE_SITUATION_CREDIT_NOTE)) {
// Select the last situation invoice
$sqlSit = 'SELECT MAX(fs.rowid)';
$sqlSit.= " FROM ".MAIN_DB_PREFIX."facture as fs";
- $sqlSit.= " WHERE fs.entity = ".$conf->entity;
+ $sqlSit.= " WHERE fs.entity IN (".getEntity('invoice').")";
$sqlSit.= " AND fs.type = ".self::TYPE_SITUATION;
$sqlSit.= " AND fs.fk_statut in (".self::STATUS_VALIDATED.",".self::STATUS_CLOSED.")";
$sqlSit.= " GROUP BY fs.situation_cycle_ref";
@@ -4231,7 +4234,7 @@ class Facture extends CommonInvoice
public function newCycle()
{
$sql = 'SELECT max(situation_cycle_ref) FROM ' . MAIN_DB_PREFIX . 'facture as f';
- $sql.= " WHERE f.entity in (".getEntity('invoice', 0).")";
+ $sql.= " WHERE f.entity IN (".getEntity('invoice', 0).")";
$resql = $this->db->query($sql);
if ($resql) {
if ($resql->num_rows > 0)
@@ -4275,8 +4278,8 @@ class Facture extends CommonInvoice
global $conf;
$sql = 'SELECT rowid FROM ' . MAIN_DB_PREFIX . 'facture';
- $sql .= ' where situation_cycle_ref = ' . $this->situation_cycle_ref;
- $sql .= ' and situation_counter < ' . $this->situation_counter;
+ $sql .= ' WHERE situation_cycle_ref = ' . $this->situation_cycle_ref;
+ $sql .= ' AND situation_counter < ' . $this->situation_counter;
$sql .= ' AND entity = '. ($this->entity > 0 ? $this->entity : $conf->entity);
$resql = $this->db->query($sql);
$res = array();
@@ -4357,7 +4360,9 @@ class Facture extends CommonInvoice
if (!empty($this->situation_cycle_ref)) {
// No point in testing anything if we're not inside a cycle
- $sql = 'SELECT max(situation_counter) FROM ' . MAIN_DB_PREFIX . 'facture WHERE situation_cycle_ref = ' . $this->situation_cycle_ref . ' AND entity = ' . ($this->entity > 0 ? $this->entity : $conf->entity);
+ $sql = 'SELECT max(situation_counter) FROM ' . MAIN_DB_PREFIX . 'facture';
+ $sql.= ' WHERE situation_cycle_ref = ' . $this->situation_cycle_ref;
+ $sql.= ' AND entity = ' . ($this->entity > 0 ? $this->entity : $conf->entity);
$resql = $this->db->query($sql);
if ($resql && $resql->num_rows > 0) {
diff --git a/htdocs/compta/facture/document.php b/htdocs/compta/facture/document.php
index 44f69f512bd..9a8f308a0d1 100644
--- a/htdocs/compta/facture/document.php
+++ b/htdocs/compta/facture/document.php
@@ -97,7 +97,7 @@ if ($id > 0 || ! empty($ref))
{
$object->fetch_thirdparty();
- $upload_dir = $conf->facture->dir_output.'/'.dol_sanitizeFileName($object->ref);
+ $upload_dir = $conf->facture->multidir_output[$object->entity].'/'.dol_sanitizeFileName($object->ref);
$head = facture_prepare_head($object);
dol_fiche_head($head, 'documents', $langs->trans('InvoiceCustomer'), -1, 'bill');
diff --git a/htdocs/compta/facture/list.php b/htdocs/compta/facture/list.php
index eca39ade98c..71e0de738fa 100644
--- a/htdocs/compta/facture/list.php
+++ b/htdocs/compta/facture/list.php
@@ -1231,7 +1231,7 @@ if ($resql)
// Extra fields
include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_print_fields.tpl.php';
// Fields from hook
- $parameters=array('arrayfields'=>$arrayfields, 'obj'=>$obj);
+ $parameters=array('arrayfields'=>$arrayfields, 'obj'=>$obj, 'i'=>$i);
$reshook=$hookmanager->executeHooks('printFieldListValue', $parameters); // Note that $action and $object may have been modified by hook
print $hookmanager->resPrint;
// Date creation
diff --git a/htdocs/compta/sociales/class/chargesociales.class.php b/htdocs/compta/sociales/class/chargesociales.class.php
index 41c9555ccfc..4fd157de9e6 100644
--- a/htdocs/compta/sociales/class/chargesociales.class.php
+++ b/htdocs/compta/sociales/class/chargesociales.class.php
@@ -317,7 +317,7 @@ class ChargeSociales extends CommonObject
$sql.= ", date_ech='".$this->db->idate($this->date_ech)."'";
$sql.= ", periode='".$this->db->idate($this->periode)."'";
$sql.= ", amount='".price2num($this->amount, 'MT')."'";
- $sql.= ", fk_projet='".$this->db->escape($this->fk_project)."'";
+ $sql.= ", fk_projet=".($this->fk_project > 0 ? $this->fk_project : null);
$sql.= ", fk_user_modif=".$user->id;
$sql.= " WHERE rowid=".$this->id;
diff --git a/htdocs/contact/class/contact.class.php b/htdocs/contact/class/contact.class.php
index 19437fe1aee..e064b93504d 100644
--- a/htdocs/contact/class/contact.class.php
+++ b/htdocs/contact/class/contact.class.php
@@ -1217,11 +1217,12 @@ class Contact extends CommonObject
public function getCivilityLabel()
{
global $langs;
- $langs->load("dict");
- $code=(! empty($this->civility_id)?$this->civility:(! empty($this->civilite)?$this->civilite:''));
+ $code=($this->civility_code ? $this->civility_code : (! empty($this->civility_id)?$this->civility:(! empty($this->civilite)?$this->civilite:'')));
if (empty($code)) return '';
- return $langs->getLabelFromKey($this->db, "Civility".$code, "c_civility", "code", "label", $code);
+
+ $langs->load("dict");
+ return $langs->getLabelFromKey($this->db, "Civility".$code, "c_civility", "code", "label", $code);
}
/**
diff --git a/htdocs/core/actions_linkedfiles.inc.php b/htdocs/core/actions_linkedfiles.inc.php
index 72c78cb8a4d..54243494de4 100644
--- a/htdocs/core/actions_linkedfiles.inc.php
+++ b/htdocs/core/actions_linkedfiles.inc.php
@@ -125,8 +125,7 @@ if ($action == 'confirm_deletefile' && $confirm == 'yes')
{
require_once DOL_DOCUMENT_ROOT . '/core/class/link.class.php';
$link = new Link($db);
- $link->id = $linkid;
- $link->fetch();
+ $link->fetch($linkid);
$res = $link->delete($user);
$langs->load('link');
@@ -160,8 +159,7 @@ elseif ($action == 'confirm_updateline' && GETPOST('save', 'alpha') && GETPOST('
require_once DOL_DOCUMENT_ROOT . '/core/class/link.class.php';
$langs->load('link');
$link = new Link($db);
- $link->id = GETPOST('linkid', 'int');
- $f = $link->fetch();
+ $f = $link->fetch(GETPOST('linkid', 'int'));
if ($f)
{
$link->url = GETPOST('link', 'alpha');
@@ -169,7 +167,7 @@ elseif ($action == 'confirm_updateline' && GETPOST('save', 'alpha') && GETPOST('
{
$link->url = 'http://' . $link->url;
}
- $link->label = GETPOST('label', 'alpha');
+ $link->label = GETPOST('label', 'alphanohtml');
$res = $link->update($user);
if (!$res)
{
diff --git a/htdocs/core/ajax/pingresult.php b/htdocs/core/ajax/pingresult.php
index 9b46546f5c3..3055942f89a 100644
--- a/htdocs/core/ajax/pingresult.php
+++ b/htdocs/core/ajax/pingresult.php
@@ -17,8 +17,7 @@
/**
* \file htdocs/core/ajax/pingresult.php
- * \brief File to save result of anonymous ping
- * Example: captureserver/public/index.php?action=dolibarrping
+ * \brief File to save result of an anonymous ping into database (1 ping is done per installation)
*/
if (! defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1'); // Disables token renewal
diff --git a/htdocs/core/boxes/box_graph_invoices_permonth.php b/htdocs/core/boxes/box_graph_invoices_permonth.php
index 5c747065138..6e5afe5df99 100644
--- a/htdocs/core/boxes/box_graph_invoices_permonth.php
+++ b/htdocs/core/boxes/box_graph_invoices_permonth.php
@@ -74,6 +74,9 @@ class box_graph_invoices_permonth extends ModeleBoxes
//include_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
//$facturestatic=new Facture($db);
+ $startmonth = $conf->global->SOCIETE_FISCAL_MONTH_START?($conf->global->SOCIETE_FISCAL_MONTH_START) : 1;
+ if (empty($conf->global->GRAPH_USE_FISCAL_YEAR)) $startmonth = 1;
+
$text = $langs->trans("BoxCustomersInvoicesPerMonth", $max);
$this->info_box_head = array(
'text' => $text,
@@ -129,7 +132,7 @@ class box_graph_invoices_permonth extends ModeleBoxes
// Build graphic number of object. $data = array(array('Lib',val1,val2,val3),...)
if ($shownb)
{
- $data1 = $stats->getNbByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0));
+ $data1 = $stats->getNbByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0), $startmonth);
$filenamenb = $dir."/".$prefix."invoicesnbinyear-".$endyear.".png";
if ($mode == 'customer') $fileurlnb = DOL_URL_ROOT.'/viewimage.php?modulepart=billstats&file=invoicesnbinyear-'.$endyear.'.png';
@@ -146,7 +149,14 @@ class box_graph_invoices_permonth extends ModeleBoxes
$i=$startyear;$legend=array();
while ($i <= $endyear)
{
- $legend[]=$i;
+ if ($startmonth != 1)
+ {
+ $legend[]=sprintf("%d/%d", $i-2001, $i-2000);
+ }
+ else
+ {
+ $legend[]=$i;
+ }
$i++;
}
$px1->SetLegend($legend);
@@ -167,7 +177,7 @@ class box_graph_invoices_permonth extends ModeleBoxes
// Build graphic number of object. $data = array(array('Lib',val1,val2,val3),...)
if ($showtot)
{
- $data2 = $stats->getAmountByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0));
+ $data2 = $stats->getAmountByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0), $startmonth);
$filenamenb = $dir."/".$prefix."invoicesamountinyear-".$endyear.".png";
if ($mode == 'customer') $fileurlnb = DOL_URL_ROOT.'/viewimage.php?modulepart=billstats&file=invoicesamountinyear-'.$endyear.'.png';
@@ -184,7 +194,14 @@ class box_graph_invoices_permonth extends ModeleBoxes
$i=$startyear;$legend=array();
while ($i <= $endyear)
{
- $legend[]=$i;
+ if ($startmonth != 1)
+ {
+ $legend[]=sprintf("%d/%d", $i-2001, $i-2000);
+ }
+ else
+ {
+ $legend[]=$i;
+ }
$i++;
}
$px2->SetLegend($legend);
diff --git a/htdocs/core/boxes/box_graph_invoices_supplier_permonth.php b/htdocs/core/boxes/box_graph_invoices_supplier_permonth.php
index d06098daa3b..df4f4afc146 100644
--- a/htdocs/core/boxes/box_graph_invoices_supplier_permonth.php
+++ b/htdocs/core/boxes/box_graph_invoices_supplier_permonth.php
@@ -73,6 +73,9 @@ class box_graph_invoices_supplier_permonth extends ModeleBoxes
include_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
+ $startmonth = $conf->global->SOCIETE_FISCAL_MONTH_START?($conf->global->SOCIETE_FISCAL_MONTH_START) : 1;
+ if (empty($conf->global->GRAPH_USE_FISCAL_YEAR)) $startmonth = 1;
+
$text = $langs->trans("BoxSuppliersInvoicesPerMonth", $max);
$this->info_box_head = array(
'text' => $text,
@@ -126,7 +129,7 @@ class box_graph_invoices_supplier_permonth extends ModeleBoxes
// Build graphic number of object. $data = array(array('Lib',val1,val2,val3),...)
if ($shownb)
{
- $data1 = $stats->getNbByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0));
+ $data1 = $stats->getNbByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0), $startmonth);
$filenamenb = $dir."/".$prefix."invoicessuppliernbinyear-".$year.".png";
if ($mode == 'customer') $fileurlnb = DOL_URL_ROOT.'/viewimage.php?modulepart=billstats&file=invoicesnbinyear-'.$year.'.png';
@@ -143,7 +146,14 @@ class box_graph_invoices_supplier_permonth extends ModeleBoxes
$i=$startyear;$legend=array();
while ($i <= $endyear)
{
- $legend[]=$i;
+ if ($startmonth != 1)
+ {
+ $legend[]=sprintf("%d/%d", $i-2001, $i-2000);
+ }
+ else
+ {
+ $legend[]=$i;
+ }
$i++;
}
$px1->SetLegend($legend);
@@ -164,7 +174,7 @@ class box_graph_invoices_supplier_permonth extends ModeleBoxes
// Build graphic number of object. $data = array(array('Lib',val1,val2,val3),...)
if ($showtot)
{
- $data2 = $stats->getAmountByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0));
+ $data2 = $stats->getAmountByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0), $startmonth);
$filenamenb = $dir."/".$prefix."invoicessupplieramountinyear-".$year.".png";
if ($mode == 'customer') $fileurlnb = DOL_URL_ROOT.'/viewimage.php?modulepart=billstats&file=invoicesamountinyear-'.$year.'.png';
@@ -181,7 +191,14 @@ class box_graph_invoices_supplier_permonth extends ModeleBoxes
$i=$startyear;$legend=array();
while ($i <= $endyear)
{
- $legend[]=$i;
+ if ($startmonth != 1)
+ {
+ $legend[]=sprintf("%d/%d", $i-2001, $i-2000);
+ }
+ else
+ {
+ $legend[]=$i;
+ }
$i++;
}
$px2->SetLegend($legend);
diff --git a/htdocs/core/boxes/box_graph_orders_permonth.php b/htdocs/core/boxes/box_graph_orders_permonth.php
index 8c4cb250376..b530b4ea268 100644
--- a/htdocs/core/boxes/box_graph_orders_permonth.php
+++ b/htdocs/core/boxes/box_graph_orders_permonth.php
@@ -74,6 +74,9 @@ class box_graph_orders_permonth extends ModeleBoxes
//include_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
//$commandestatic=new Commande($db);
+ $startmonth = $conf->global->SOCIETE_FISCAL_MONTH_START?($conf->global->SOCIETE_FISCAL_MONTH_START) : 1;
+ if (empty($conf->global->GRAPH_USE_FISCAL_YEAR)) $startmonth = 1;
+
$text = $langs->trans("BoxCustomersOrdersPerMonth", $max);
$this->info_box_head = array(
'text' => $text,
@@ -129,7 +132,7 @@ class box_graph_orders_permonth extends ModeleBoxes
// Build graphic number of object. $data = array(array('Lib',val1,val2,val3),...)
if ($shownb)
{
- $data1 = $stats->getNbByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0));
+ $data1 = $stats->getNbByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0), $startmonth);
$filenamenb = $dir."/".$prefix."ordersnbinyear-".$endyear.".png";
if ($mode == 'customer') $fileurlnb = DOL_URL_ROOT.'/viewimage.php?modulepart=orderstats&file=ordersnbinyear-'.$endyear.'.png';
@@ -144,7 +147,14 @@ class box_graph_orders_permonth extends ModeleBoxes
$i=$startyear;$legend=array();
while ($i <= $endyear)
{
- $legend[]=$i;
+ if ($startmonth != 1)
+ {
+ $legend[]=sprintf("%d/%d", $i-2001, $i-2000);
+ }
+ else
+ {
+ $legend[]=$i;
+ }
$i++;
}
$px1->SetLegend($legend);
@@ -165,7 +175,7 @@ class box_graph_orders_permonth extends ModeleBoxes
// Build graphic number of object. $data = array(array('Lib',val1,val2,val3),...)
if ($showtot)
{
- $data2 = $stats->getAmountByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0));
+ $data2 = $stats->getAmountByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0), $startmonth);
$filenamenb = $dir."/".$prefix."ordersamountinyear-".$endyear.".png";
if ($mode == 'customer') $fileurlnb = DOL_URL_ROOT.'/viewimage.php?modulepart=orderstats&file=ordersamountinyear-'.$endyear.'.png';
@@ -180,7 +190,14 @@ class box_graph_orders_permonth extends ModeleBoxes
$i=$startyear;$legend=array();
while ($i <= $endyear)
{
- $legend[]=$i;
+ if ($startmonth != 1)
+ {
+ $legend[]=sprintf("%d/%d", $i-2001, $i-2000);
+ }
+ else
+ {
+ $legend[]=$i;
+ }
$i++;
}
$px2->SetLegend($legend);
diff --git a/htdocs/core/boxes/box_graph_orders_supplier_permonth.php b/htdocs/core/boxes/box_graph_orders_supplier_permonth.php
index a584c7288ca..8359ddbe070 100644
--- a/htdocs/core/boxes/box_graph_orders_supplier_permonth.php
+++ b/htdocs/core/boxes/box_graph_orders_supplier_permonth.php
@@ -73,6 +73,9 @@ class box_graph_orders_supplier_permonth extends ModeleBoxes
include_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.commande.class.php';
+ $startmonth = $conf->global->SOCIETE_FISCAL_MONTH_START?($conf->global->SOCIETE_FISCAL_MONTH_START) : 1;
+ if (empty($conf->global->GRAPH_USE_FISCAL_YEAR)) $startmonth = 1;
+
$text = $langs->trans("BoxSuppliersOrdersPerMonth", $max);
$this->info_box_head = array(
'text' => $text,
@@ -128,7 +131,7 @@ class box_graph_orders_supplier_permonth extends ModeleBoxes
// Build graphic number of object. $data = array(array('Lib',val1,val2,val3),...)
if ($shownb)
{
- $data1 = $stats->getNbByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0));
+ $data1 = $stats->getNbByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0), $startmonth);
$filenamenb = $dir."/".$prefix."orderssuppliernbinyear-".$endyear.".png";
if ($mode == 'customer') $fileurlnb = DOL_URL_ROOT.'/viewimage.php?modulepart=orderstats&file=ordersnbinyear-'.$endyear.'.png';
@@ -143,7 +146,14 @@ class box_graph_orders_supplier_permonth extends ModeleBoxes
$i=$startyear;$legend=array();
while ($i <= $endyear)
{
- $legend[]=$i;
+ if ($startmonth != 1)
+ {
+ $legend[]=sprintf("%d/%d", $i-2001, $i-2000);
+ }
+ else
+ {
+ $legend[]=$i;
+ }
$i++;
}
$px1->SetLegend($legend);
@@ -164,7 +174,7 @@ class box_graph_orders_supplier_permonth extends ModeleBoxes
// Build graphic number of object. $data = array(array('Lib',val1,val2,val3),...)
if ($showtot)
{
- $data2 = $stats->getAmountByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0));
+ $data2 = $stats->getAmountByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0), $startmonth);
$filenamenb = $dir."/".$prefix."orderssupplieramountinyear-".$endyear.".png";
if ($mode == 'customer') $fileurlnb = DOL_URL_ROOT.'/viewimage.php?modulepart=orderstats&file=ordersamountinyear-'.$endyear.'.png';
@@ -179,7 +189,14 @@ class box_graph_orders_supplier_permonth extends ModeleBoxes
$i=$startyear;$legend=array();
while ($i <= $endyear)
{
- $legend[]=$i;
+ if ($startmonth != 1)
+ {
+ $legend[]=sprintf("%d/%d", $i-2001, $i-2000);
+ }
+ else
+ {
+ $legend[]=$i;
+ }
$i++;
}
$px2->SetLegend($legend);
diff --git a/htdocs/core/boxes/box_graph_propales_permonth.php b/htdocs/core/boxes/box_graph_propales_permonth.php
index 861c5086704..75673ee3fce 100644
--- a/htdocs/core/boxes/box_graph_propales_permonth.php
+++ b/htdocs/core/boxes/box_graph_propales_permonth.php
@@ -74,6 +74,9 @@ class box_graph_propales_permonth extends ModeleBoxes
//include_once DOL_DOCUMENT_ROOT.'/comm/propal/class/propal.class.php';
//$propalstatic=new Propal($db);
+ $startmonth = $conf->global->SOCIETE_FISCAL_MONTH_START?($conf->global->SOCIETE_FISCAL_MONTH_START) : 1;
+ if (empty($conf->global->GRAPH_USE_FISCAL_YEAR)) $startmonth = 1;
+
$langs->load("propal");
$text = $langs->trans("BoxProposalsPerMonth", $max);
@@ -128,7 +131,7 @@ class box_graph_propales_permonth extends ModeleBoxes
// Build graphic number of object. $data = array(array('Lib',val1,val2,val3),...)
if ($shownb)
{
- $data1 = $stats->getNbByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0));
+ $data1 = $stats->getNbByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0), $startmonth);
$datatype1 = array_pad(array(), ($endyear-$startyear+1), 'bars');
$filenamenb = $dir."/".$prefix."propalsnbinyear-".$endyear.".png";
@@ -144,7 +147,14 @@ class box_graph_propales_permonth extends ModeleBoxes
$i=$startyear;$legend=array();
while ($i <= $endyear)
{
- $legend[]=$i;
+ if ($startmonth != 1)
+ {
+ $legend[]=sprintf("%d/%d", $i-2001, $i-2000);
+ }
+ else
+ {
+ $legend[]=$i;
+ }
$i++;
}
$px1->SetLegend($legend);
@@ -165,7 +175,7 @@ class box_graph_propales_permonth extends ModeleBoxes
// Build graphic number of object. $data = array(array('Lib',val1,val2,val3),...)
if ($showtot)
{
- $data2 = $stats->getAmountByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0));
+ $data2 = $stats->getAmountByMonthWithPrevYear($endyear, $startyear, (GETPOST('action', 'aZ09')==$refreshaction?-1:(3600*24)), ($WIDTH<300?2:0), $startmonth);
$datatype2 = array_pad(array(), ($endyear-$startyear+1), 'bars');
//$datatype2 = array('lines','bars');
@@ -183,7 +193,14 @@ class box_graph_propales_permonth extends ModeleBoxes
$i=$startyear;$legend=array();
while ($i <= $endyear)
{
- $legend[]=$i;
+ if ($startmonth != 1)
+ {
+ $legend[]=sprintf("%d/%d", $i-2001, $i-2000);
+ }
+ else
+ {
+ $legend[]=$i;
+ }
$i++;
}
$px2->SetLegend($legend);
diff --git a/htdocs/core/class/commonobject.class.php b/htdocs/core/class/commonobject.class.php
index e6d47c53d5f..458c6a2563b 100644
--- a/htdocs/core/class/commonobject.class.php
+++ b/htdocs/core/class/commonobject.class.php
@@ -1913,7 +1913,7 @@ abstract class CommonObject
* Change the multicurrency rate
*
* @param double $rate multicurrency rate
- * @param int $mode mode 1 : amounts in company currency will be recalculated, mode 2 : amounts in foreign currency
+ * @param int $mode mode 1 : amounts in company currency will be recalculated, mode 2 : amounts in foreign currency will be recalculated
* @return int >0 if OK, <0 if KO
*/
public function setMulticurrencyRate($rate, $mode = 1)
@@ -1936,10 +1936,16 @@ abstract class CommonObject
{
foreach ($this->lines as &$line)
{
+ // Amounts in company currency will be recalculated
if($mode == 1) {
$line->subprice = 0;
}
+ // Amounts in foreign currency will be recalculated
+ if($mode == 2) {
+ $line->multicurrency_subprice = 0;
+ }
+
switch ($this->element) {
case 'propal':
$this->updateline(
diff --git a/htdocs/core/class/conf.class.php b/htdocs/core/class/conf.class.php
index 0d76b83cb8a..3088d8a95ce 100644
--- a/htdocs/core/class/conf.class.php
+++ b/htdocs/core/class/conf.class.php
@@ -327,6 +327,9 @@ class Conf
}
// For mycompany storage
+ $this->mycompany->multidir_output = array($this->entity => $rootfordata."/mycompany");
+ $this->mycompany->multidir_temp = array($this->entity => $rootfordata."/mycompany/temp");
+ // For backward compatibility
$this->mycompany->dir_output=$rootfordata."/mycompany";
$this->mycompany->dir_temp=$rootfordata."/mycompany/temp";
diff --git a/htdocs/core/class/dolgraph.class.php b/htdocs/core/class/dolgraph.class.php
index 458f0bd4fad..5c45eb5836d 100644
--- a/htdocs/core/class/dolgraph.class.php
+++ b/htdocs/core/class/dolgraph.class.php
@@ -1094,7 +1094,7 @@ class DolGraph
$i++;
}
// shadowSize: 0 -> Drawing is faster without shadows
- $this->stringtoshow.="\n".' ], { series: { shadowSize: 0, stack: stack, lines: { fill: false, steps: steps }, bars: { barWidth: 0.6 } }'."\n";
+ $this->stringtoshow.="\n".' ], { series: { shadowSize: 0, stack: stack, lines: { fill: false, steps: steps }, bars: { barWidth: 0.6, fillColor: { colors: [{opacity: 0.9 }, {opacity: 0.85}] }} }'."\n";
// Xaxis
$this->stringtoshow.=', xaxis: { ticks: ['."\n";
diff --git a/htdocs/core/class/html.form.class.php b/htdocs/core/class/html.form.class.php
index e829fdf6661..3a69d8e6d42 100644
--- a/htdocs/core/class/html.form.class.php
+++ b/htdocs/core/class/html.form.class.php
@@ -307,7 +307,7 @@ class Form
* @param mixed $custommsg String or Array of custom messages : eg array('success' => 'MyMessage', 'error' => 'MyMessage')
* @return string HTML edit in place
*/
- private function editInPlace($object, $value, $htmlname, $condition, $inputType = 'textarea', $editvalue = null, $extObject = null, $custommsg = null)
+ protected function editInPlace($object, $value, $htmlname, $condition, $inputType = 'textarea', $editvalue = null, $extObject = null, $custommsg = null)
{
global $conf;
@@ -523,7 +523,7 @@ class Form
* @param string $extracss Add a CSS style to td, div or span tag
* @param int $noencodehtmltext Do not encode into html entity the htmltext
* @param int $notabs 0=Include table and tr tags, 1=Do not include table and tr tags, 2=use div, 3=use span
- * @param string $tooltiptrigger ''=Tooltip on hover, 'abc'=Tooltip on click (abc is a unique key)
+ * @param string $tooltiptrigger ''=Tooltip on hover, 'abc'=Tooltip on click (abc is a unique key, clickable link is on image or on link if param $type='none')
* @param int $forcenowrap Force no wrap between text and picto (works with notabs=2 only)
* @return string HTML code of text, picto, tooltip
*/
@@ -2362,7 +2362,7 @@ class Form
* @param string $filterkey Filter key to highlight
* @return void
*/
- private function constructProductListOption(&$objp, &$opt, &$optJson, $price_level, $selected, $hidepriceinlabel = 0, $filterkey = '')
+ protected function constructProductListOption(&$objp, &$opt, &$optJson, $price_level, $selected, $hidepriceinlabel = 0, $filterkey = '')
{
global $langs, $conf, $user, $db;
diff --git a/htdocs/core/class/html.formfile.class.php b/htdocs/core/class/html.formfile.class.php
index 5f63d4db66c..1622ee88c2f 100644
--- a/htdocs/core/class/html.formfile.class.php
+++ b/htdocs/core/class/html.formfile.class.php
@@ -1258,7 +1258,7 @@ class FormFile
print '';
}
// Preview link
- if (! $editline) print $this->showPreview($file, $modulepart, $filepath);
+ if (! $editline) print $this->showPreview($file, $modulepart, $filepath, 0, '&entity='.(! empty($object->entity)?$object->entity:$conf->entity));
print "\n";
@@ -1839,7 +1839,7 @@ class FormFile
print $langs->trans('Link') . ':
';
print '';
print '
';
- print $langs->trans('Label') . ': ';
+ print $langs->trans('Label') . ': ';
print ' ';
print '
' . dol_print_date(dol_now(), "dayhour", "tzuser") . ' ';
print '
';
@@ -1853,7 +1853,7 @@ class FormFile
print '
';
print img_picto('', 'object_globe').' ';
print '';
- print $link->label;
+ print dol_escape_htmltag($link->label);
print ' ';
print ' '."\n";
print '
';
diff --git a/htdocs/core/class/html.formprojet.class.php b/htdocs/core/class/html.formprojet.class.php
index 8e01e93b66e..d47be50b3d0 100644
--- a/htdocs/core/class/html.formprojet.class.php
+++ b/htdocs/core/class/html.formprojet.class.php
@@ -153,7 +153,7 @@ class FormProjets
$outarray=array();
$hideunselectables = false;
- if (! empty($conf->global->CONTRACT_HIDE_UNSELECTABLES)) $hideunselectables = true;
+ if (! empty($conf->global->PROJECT_HIDE_UNSELECTABLES)) $hideunselectables = true;
$projectsListId = false;
if (empty($user->rights->projet->all->lire))
diff --git a/htdocs/core/class/html.formwebsite.class.php b/htdocs/core/class/html.formwebsite.class.php
index 3cefeaef99c..42c3b36025b 100644
--- a/htdocs/core/class/html.formwebsite.class.php
+++ b/htdocs/core/class/html.formwebsite.class.php
@@ -176,7 +176,21 @@ class FormWebsite
$langs->load("admin");
- $arrayofsamples=array('empty'=>'EmptyPage', 'corporatehome'=>'CorporateHomePage');
+ $listofsamples = dol_dir_list(DOL_DOCUMENT_ROOT.'/website/samples', 'files', 0, '^page-sample-.*\.html$');
+
+ $arrayofsamples = array();
+ $arrayofsamples['empty']='EmptyPage'; // Always this one first
+ foreach($listofsamples as $sample)
+ {
+ $reg = array();
+ if (preg_match('/^page-sample-(.*)\.html$/', $sample['name'], $reg))
+ {
+ $key = $reg[1];
+ $labelkey = ucfirst($key);
+ if ($key == 'empty') $labelkey = 'EmptyPage';
+ $arrayofsamples[$key] = $labelkey;
+ }
+ }
$out = '';
$out .= '
';
diff --git a/htdocs/core/class/lessc.class.php b/htdocs/core/class/lessc.class.php
new file mode 100644
index 00000000000..34034226497
--- /dev/null
+++ b/htdocs/core/class/lessc.class.php
@@ -0,0 +1,3886 @@
+
+ * Licensed under MIT or GPLv3, see LICENSE
+ */
+
+
+/**
+ * The LESS compiler and parser.
+ *
+ * Converting LESS to CSS is a three stage process. The incoming file is parsed
+ * by `lessc_parser` into a syntax tree, then it is compiled into another tree
+ * representing the CSS structure by `lessc`. The CSS tree is fed into a
+ * formatter, like `lessc_formatter` which then outputs CSS as a string.
+ *
+ * During the first compile, all values are *reduced*, which means that their
+ * types are brought to the lowest form before being dump as strings. This
+ * handles math equations, variable dereferences, and the like.
+ *
+ * The `parse` function of `lessc` is the entry point.
+ *
+ * In summary:
+ *
+ * The `lessc` class creates an instance of the parser, feeds it LESS code,
+ * then transforms the resulting tree to a CSS tree. This class also holds the
+ * evaluation context, such as all available mixins and variables at any given
+ * time.
+ *
+ * The `lessc_parser` class is only concerned with parsing its input.
+ *
+ * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string,
+ * handling things like indentation.
+ */
+class lessc {
+ static public $VERSION = "v0.5.0";
+
+ static public $TRUE = array("keyword", "true");
+ static public $FALSE = array("keyword", "false");
+
+ protected $libFunctions = array();
+ protected $registeredVars = array();
+ protected $preserveComments = false;
+
+ public $vPrefix = '@'; // prefix of abstract properties
+ public $mPrefix = '$'; // prefix of abstract blocks
+ public $parentSelector = '&';
+
+ public $importDisabled = false;
+ public $importDir = '';
+
+ protected $numberPrecision = null;
+
+ protected $allParsedFiles = array();
+
+ // set to the parser that generated the current line when compiling
+ // so we know how to create error messages
+ protected $sourceParser = null;
+ protected $sourceLoc = null;
+
+ static protected $nextImportId = 0; // uniquely identify imports
+
+ // attempts to find the path of an import url, returns null for css files
+ protected function findImport($url) {
+ foreach ((array)$this->importDir as $dir) {
+ $full = $dir.(substr($dir, -1) != '/' ? '/' : '').$url;
+ if ($this->fileExists($file = $full.'.less') || $this->fileExists($file = $full)) {
+ return $file;
+ }
+ }
+
+ return null;
+ }
+
+ protected function fileExists($name) {
+ return is_file($name);
+ }
+
+ public static function compressList($items, $delim) {
+ if (!isset($items[1]) && isset($items[0])) return $items[0];
+ else return array('list', $delim, $items);
+ }
+
+ public static function preg_quote($what) {
+ return preg_quote($what, '/');
+ }
+
+ protected function tryImport($importPath, $parentBlock, $out) {
+ if ($importPath[0] == "function" && $importPath[1] == "url") {
+ $importPath = $this->flattenList($importPath[2]);
+ }
+
+ $str = $this->coerceString($importPath);
+ if ($str === null) return false;
+
+ $url = $this->compileValue($this->lib_e($str));
+
+ // don't import if it ends in css
+ if (substr_compare($url, '.css', -4, 4) === 0) return false;
+
+ $realPath = $this->findImport($url);
+
+ if ($realPath === null) return false;
+
+ if ($this->importDisabled) {
+ return array(false, "/* import disabled */");
+ }
+
+ if (isset($this->allParsedFiles[realpath($realPath)])) {
+ return array(false, null);
+ }
+
+ $this->addParsedFile($realPath);
+ $parser = $this->makeParser($realPath);
+ $root = $parser->parse(file_get_contents($realPath));
+
+ // set the parents of all the block props
+ foreach ($root->props as $prop) {
+ if ($prop[0] == "block") {
+ $prop[1]->parent = $parentBlock;
+ }
+ }
+
+ // copy mixins into scope, set their parents
+ // bring blocks from import into current block
+ // TODO: need to mark the source parser these came from this file
+ foreach ($root->children as $childName => $child) {
+ if (isset($parentBlock->children[$childName])) {
+ $parentBlock->children[$childName] = array_merge(
+ $parentBlock->children[$childName],
+ $child);
+ } else {
+ $parentBlock->children[$childName] = $child;
+ }
+ }
+
+ $pi = pathinfo($realPath);
+ $dir = $pi["dirname"];
+
+ list($top, $bottom) = $this->sortProps($root->props, true);
+ $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir);
+
+ return array(true, $bottom, $parser, $dir);
+ }
+
+ protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir) {
+ $oldSourceParser = $this->sourceParser;
+
+ $oldImport = $this->importDir;
+
+ // TODO: this is because the importDir api is stupid
+ $this->importDir = (array)$this->importDir;
+ array_unshift($this->importDir, $importDir);
+
+ foreach ($props as $prop) {
+ $this->compileProp($prop, $block, $out);
+ }
+
+ $this->importDir = $oldImport;
+ $this->sourceParser = $oldSourceParser;
+ }
+
+ /**
+ * Recursively compiles a block.
+ *
+ * A block is analogous to a CSS block in most cases. A single LESS document
+ * is encapsulated in a block when parsed, but it does not have parent tags
+ * so all of it's children appear on the root level when compiled.
+ *
+ * Blocks are made up of props and children.
+ *
+ * Props are property instructions, array tuples which describe an action
+ * to be taken, eg. write a property, set a variable, mixin a block.
+ *
+ * The children of a block are just all the blocks that are defined within.
+ * This is used to look up mixins when performing a mixin.
+ *
+ * Compiling the block involves pushing a fresh environment on the stack,
+ * and iterating through the props, compiling each one.
+ *
+ * See lessc::compileProp()
+ *
+ */
+ protected function compileBlock($block) {
+ switch ($block->type) {
+ case "root":
+ $this->compileRoot($block);
+ break;
+ case null:
+ $this->compileCSSBlock($block);
+ break;
+ case "media":
+ $this->compileMedia($block);
+ break;
+ case "directive":
+ $name = "@" . $block->name;
+ if (!empty($block->value)) {
+ $name .= " " . $this->compileValue($this->reduce($block->value));
+ }
+
+ $this->compileNestedBlock($block, array($name));
+ break;
+ default:
+ $this->throwError("unknown block type: $block->type\n");
+ }
+ }
+
+ protected function compileCSSBlock($block) {
+ $env = $this->pushEnv();
+
+ $selectors = $this->compileSelectors($block->tags);
+ $env->selectors = $this->multiplySelectors($selectors);
+ $out = $this->makeOutputBlock(null, $env->selectors);
+
+ $this->scope->children[] = $out;
+ $this->compileProps($block, $out);
+
+ $block->scope = $env; // mixins carry scope with them!
+ $this->popEnv();
+ }
+
+ protected function compileMedia($media) {
+ $env = $this->pushEnv($media);
+ $parentScope = $this->mediaParent($this->scope);
+
+ $query = $this->compileMediaQuery($this->multiplyMedia($env));
+
+ $this->scope = $this->makeOutputBlock($media->type, array($query));
+ $parentScope->children[] = $this->scope;
+
+ $this->compileProps($media, $this->scope);
+
+ if (count($this->scope->lines) > 0) {
+ $orphanSelelectors = $this->findClosestSelectors();
+ if (!is_null($orphanSelelectors)) {
+ $orphan = $this->makeOutputBlock(null, $orphanSelelectors);
+ $orphan->lines = $this->scope->lines;
+ array_unshift($this->scope->children, $orphan);
+ $this->scope->lines = array();
+ }
+ }
+
+ $this->scope = $this->scope->parent;
+ $this->popEnv();
+ }
+
+ protected function mediaParent($scope) {
+ while (!empty($scope->parent)) {
+ if (!empty($scope->type) && $scope->type != "media") {
+ break;
+ }
+ $scope = $scope->parent;
+ }
+
+ return $scope;
+ }
+
+ protected function compileNestedBlock($block, $selectors) {
+ $this->pushEnv($block);
+ $this->scope = $this->makeOutputBlock($block->type, $selectors);
+ $this->scope->parent->children[] = $this->scope;
+
+ $this->compileProps($block, $this->scope);
+
+ $this->scope = $this->scope->parent;
+ $this->popEnv();
+ }
+
+ protected function compileRoot($root) {
+ $this->pushEnv();
+ $this->scope = $this->makeOutputBlock($root->type);
+ $this->compileProps($root, $this->scope);
+ $this->popEnv();
+ }
+
+ protected function compileProps($block, $out) {
+ foreach ($this->sortProps($block->props) as $prop) {
+ $this->compileProp($prop, $block, $out);
+ }
+ $out->lines = $this->deduplicate($out->lines);
+ }
+
+ /**
+ * Deduplicate lines in a block. Comments are not deduplicated. If a
+ * duplicate rule is detected, the comments immediately preceding each
+ * occurence are consolidated.
+ */
+ protected function deduplicate($lines) {
+ $unique = array();
+ $comments = array();
+
+ foreach ($lines as $line) {
+ if (strpos($line, '/*') === 0) {
+ $comments[] = $line;
+ continue;
+ }
+ if (!in_array($line, $unique)) {
+ $unique[] = $line;
+ }
+ array_splice($unique, array_search($line, $unique), 0, $comments);
+ $comments = array();
+ }
+ return array_merge($unique, $comments);
+ }
+
+ protected function sortProps($props, $split = false) {
+ $vars = array();
+ $imports = array();
+ $other = array();
+ $stack = array();
+
+ foreach ($props as $prop) {
+ switch ($prop[0]) {
+ case "comment":
+ $stack[] = $prop;
+ break;
+ case "assign":
+ $stack[] = $prop;
+ if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) {
+ $vars = array_merge($vars, $stack);
+ } else {
+ $other = array_merge($other, $stack);
+ }
+ $stack = array();
+ break;
+ case "import":
+ $id = self::$nextImportId++;
+ $prop[] = $id;
+ $stack[] = $prop;
+ $imports = array_merge($imports, $stack);
+ $other[] = array("import_mixin", $id);
+ $stack = array();
+ break;
+ default:
+ $stack[] = $prop;
+ $other = array_merge($other, $stack);
+ $stack = array();
+ break;
+ }
+ }
+ $other = array_merge($other, $stack);
+
+ if ($split) {
+ return array(array_merge($imports, $vars), $other);
+ } else {
+ return array_merge($imports, $vars, $other);
+ }
+ }
+
+ protected function compileMediaQuery($queries) {
+ $compiledQueries = array();
+ foreach ($queries as $query) {
+ $parts = array();
+ foreach ($query as $q) {
+ switch ($q[0]) {
+ case "mediaType":
+ $parts[] = implode(" ", array_slice($q, 1));
+ break;
+ case "mediaExp":
+ if (isset($q[2])) {
+ $parts[] = "($q[1]: " .
+ $this->compileValue($this->reduce($q[2])) . ")";
+ } else {
+ $parts[] = "($q[1])";
+ }
+ break;
+ case "variable":
+ $parts[] = $this->compileValue($this->reduce($q));
+ break;
+ }
+ }
+
+ if (count($parts) > 0) {
+ $compiledQueries[] = implode(" and ", $parts);
+ }
+ }
+
+ $out = "@media";
+ if (!empty($parts)) {
+ $out .= " " .
+ implode($this->formatter->selectorSeparator, $compiledQueries);
+ }
+ return $out;
+ }
+
+ protected function multiplyMedia($env, $childQueries = null) {
+ if (is_null($env) ||
+ !empty($env->block->type) && $env->block->type != "media"
+ ) {
+ return $childQueries;
+ }
+
+ // plain old block, skip
+ if (empty($env->block->type)) {
+ return $this->multiplyMedia($env->parent, $childQueries);
+ }
+
+ $out = array();
+ $queries = $env->block->queries;
+ if (is_null($childQueries)) {
+ $out = $queries;
+ } else {
+ foreach ($queries as $parent) {
+ foreach ($childQueries as $child) {
+ $out[] = array_merge($parent, $child);
+ }
+ }
+ }
+
+ return $this->multiplyMedia($env->parent, $out);
+ }
+
+ protected function expandParentSelectors(&$tag, $replace) {
+ $parts = explode("$&$", $tag);
+ $count = 0;
+ foreach ($parts as &$part) {
+ $part = str_replace($this->parentSelector, $replace, $part, $c);
+ $count += $c;
+ }
+ $tag = implode($this->parentSelector, $parts);
+ return $count;
+ }
+
+ protected function findClosestSelectors() {
+ $env = $this->env;
+ $selectors = null;
+ while ($env !== null) {
+ if (isset($env->selectors)) {
+ $selectors = $env->selectors;
+ break;
+ }
+ $env = $env->parent;
+ }
+
+ return $selectors;
+ }
+
+
+ // multiply $selectors against the nearest selectors in env
+ protected function multiplySelectors($selectors) {
+ // find parent selectors
+
+ $parentSelectors = $this->findClosestSelectors();
+ if (is_null($parentSelectors)) {
+ // kill parent reference in top level selector
+ foreach ($selectors as &$s) {
+ $this->expandParentSelectors($s, "");
+ }
+
+ return $selectors;
+ }
+
+ $out = array();
+ foreach ($parentSelectors as $parent) {
+ foreach ($selectors as $child) {
+ $count = $this->expandParentSelectors($child, $parent);
+
+ // don't prepend the parent tag if & was used
+ if ($count > 0) {
+ $out[] = trim($child);
+ } else {
+ $out[] = trim($parent . ' ' . $child);
+ }
+ }
+ }
+
+ return $out;
+ }
+
+ // reduces selector expressions
+ protected function compileSelectors($selectors) {
+ $out = array();
+
+ foreach ($selectors as $s) {
+ if (is_array($s)) {
+ list(, $value) = $s;
+ $out[] = trim($this->compileValue($this->reduce($value)));
+ } else {
+ $out[] = $s;
+ }
+ }
+
+ return $out;
+ }
+
+ protected function eq($left, $right) {
+ return $left == $right;
+ }
+
+ protected function patternMatch($block, $orderedArgs, $keywordArgs) {
+ // match the guards if it has them
+ // any one of the groups must have all its guards pass for a match
+ if (!empty($block->guards)) {
+ $groupPassed = false;
+ foreach ($block->guards as $guardGroup) {
+ foreach ($guardGroup as $guard) {
+ $this->pushEnv();
+ $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs);
+
+ $negate = false;
+ if ($guard[0] == "negate") {
+ $guard = $guard[1];
+ $negate = true;
+ }
+
+ $passed = $this->reduce($guard) == self::$TRUE;
+ if ($negate) $passed = !$passed;
+
+ $this->popEnv();
+
+ if ($passed) {
+ $groupPassed = true;
+ } else {
+ $groupPassed = false;
+ break;
+ }
+ }
+
+ if ($groupPassed) break;
+ }
+
+ if (!$groupPassed) {
+ return false;
+ }
+ }
+
+ if (empty($block->args)) {
+ return $block->isVararg || empty($orderedArgs) && empty($keywordArgs);
+ }
+
+ $remainingArgs = $block->args;
+ if ($keywordArgs) {
+ $remainingArgs = array();
+ foreach ($block->args as $arg) {
+ if ($arg[0] == "arg" && isset($keywordArgs[$arg[1]])) {
+ continue;
+ }
+
+ $remainingArgs[] = $arg;
+ }
+ }
+
+ $i = -1; // no args
+ // try to match by arity or by argument literal
+ foreach ($remainingArgs as $i => $arg) {
+ switch ($arg[0]) {
+ case "lit":
+ if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) {
+ return false;
+ }
+ break;
+ case "arg":
+ // no arg and no default value
+ if (!isset($orderedArgs[$i]) && !isset($arg[2])) {
+ return false;
+ }
+ break;
+ case "rest":
+ $i--; // rest can be empty
+ break 2;
+ }
+ }
+
+ if ($block->isVararg) {
+ return true; // not having enough is handled above
+ } else {
+ $numMatched = $i + 1;
+ // greater than because default values always match
+ return $numMatched >= count($orderedArgs);
+ }
+ }
+
+ protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip=array()) {
+ $matches = null;
+ foreach ($blocks as $block) {
+ // skip seen blocks that don't have arguments
+ if (isset($skip[$block->id]) && !isset($block->args)) {
+ continue;
+ }
+
+ if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) {
+ $matches[] = $block;
+ }
+ }
+
+ return $matches;
+ }
+
+ // attempt to find blocks matched by path and args
+ protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen=array()) {
+ if ($searchIn == null) return null;
+ if (isset($seen[$searchIn->id])) return null;
+ $seen[$searchIn->id] = true;
+
+ $name = $path[0];
+
+ if (isset($searchIn->children[$name])) {
+ $blocks = $searchIn->children[$name];
+ if (count($path) == 1) {
+ $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen);
+ if (!empty($matches)) {
+ // This will return all blocks that match in the closest
+ // scope that has any matching block, like lessjs
+ return $matches;
+ }
+ } else {
+ $matches = array();
+ foreach ($blocks as $subBlock) {
+ $subMatches = $this->findBlocks($subBlock,
+ array_slice($path, 1), $orderedArgs, $keywordArgs, $seen);
+
+ if (!is_null($subMatches)) {
+ foreach ($subMatches as $sm) {
+ $matches[] = $sm;
+ }
+ }
+ }
+
+ return count($matches) > 0 ? $matches : null;
+ }
+ }
+ if ($searchIn->parent === $searchIn) return null;
+ return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen);
+ }
+
+ // sets all argument names in $args to either the default value
+ // or the one passed in through $values
+ protected function zipSetArgs($args, $orderedValues, $keywordValues) {
+ $assignedValues = array();
+
+ $i = 0;
+ foreach ($args as $a) {
+ if ($a[0] == "arg") {
+ if (isset($keywordValues[$a[1]])) {
+ // has keyword arg
+ $value = $keywordValues[$a[1]];
+ } elseif (isset($orderedValues[$i])) {
+ // has ordered arg
+ $value = $orderedValues[$i];
+ $i++;
+ } elseif (isset($a[2])) {
+ // has default value
+ $value = $a[2];
+ } else {
+ $this->throwError("Failed to assign arg " . $a[1]);
+ $value = null; // :(
+ }
+
+ $value = $this->reduce($value);
+ $this->set($a[1], $value);
+ $assignedValues[] = $value;
+ } else {
+ // a lit
+ $i++;
+ }
+ }
+
+ // check for a rest
+ $last = end($args);
+ if ($last[0] == "rest") {
+ $rest = array_slice($orderedValues, count($args) - 1);
+ $this->set($last[1], $this->reduce(array("list", " ", $rest)));
+ }
+
+ // wow is this the only true use of PHP's + operator for arrays?
+ $this->env->arguments = $assignedValues + $orderedValues;
+ }
+
+ // compile a prop and update $lines or $blocks appropriately
+ protected function compileProp($prop, $block, $out) {
+ // set error position context
+ $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1;
+
+ switch ($prop[0]) {
+ case 'assign':
+ list(, $name, $value) = $prop;
+ if ($name[0] == $this->vPrefix) {
+ $this->set($name, $value);
+ } else {
+ $out->lines[] = $this->formatter->property($name,
+ $this->compileValue($this->reduce($value)));
+ }
+ break;
+ case 'block':
+ list(, $child) = $prop;
+ $this->compileBlock($child);
+ break;
+ case 'mixin':
+ list(, $path, $args, $suffix) = $prop;
+
+ $orderedArgs = array();
+ $keywordArgs = array();
+ foreach ((array)$args as $arg) {
+ $argval = null;
+ switch ($arg[0]) {
+ case "arg":
+ if (!isset($arg[2])) {
+ $orderedArgs[] = $this->reduce(array("variable", $arg[1]));
+ } else {
+ $keywordArgs[$arg[1]] = $this->reduce($arg[2]);
+ }
+ break;
+
+ case "lit":
+ $orderedArgs[] = $this->reduce($arg[1]);
+ break;
+ default:
+ $this->throwError("Unknown arg type: " . $arg[0]);
+ }
+ }
+
+ $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs);
+
+ if ($mixins === null) {
+ $this->throwError("{$prop[1][0]} is undefined");
+ }
+
+ foreach ($mixins as $mixin) {
+ if ($mixin === $block && !$orderedArgs) {
+ continue;
+ }
+
+ $haveScope = false;
+ if (isset($mixin->parent->scope)) {
+ $haveScope = true;
+ $mixinParentEnv = $this->pushEnv();
+ $mixinParentEnv->storeParent = $mixin->parent->scope;
+ }
+
+ $haveArgs = false;
+ if (isset($mixin->args)) {
+ $haveArgs = true;
+ $this->pushEnv();
+ $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs);
+ }
+
+ $oldParent = $mixin->parent;
+ if ($mixin != $block) $mixin->parent = $block;
+
+ foreach ($this->sortProps($mixin->props) as $subProp) {
+ if ($suffix !== null &&
+ $subProp[0] == "assign" &&
+ is_string($subProp[1]) &&
+ $subProp[1]{0} != $this->vPrefix
+ ) {
+ $subProp[2] = array(
+ 'list', ' ',
+ array($subProp[2], array('keyword', $suffix))
+ );
+ }
+
+ $this->compileProp($subProp, $mixin, $out);
+ }
+
+ $mixin->parent = $oldParent;
+
+ if ($haveArgs) $this->popEnv();
+ if ($haveScope) $this->popEnv();
+ }
+
+ break;
+ case 'raw':
+ $out->lines[] = $prop[1];
+ break;
+ case "directive":
+ list(, $name, $value) = $prop;
+ $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)).';';
+ break;
+ case "comment":
+ $out->lines[] = $prop[1];
+ break;
+ case "import":
+ list(, $importPath, $importId) = $prop;
+ $importPath = $this->reduce($importPath);
+
+ if (!isset($this->env->imports)) {
+ $this->env->imports = array();
+ }
+
+ $result = $this->tryImport($importPath, $block, $out);
+
+ $this->env->imports[$importId] = $result === false ?
+ array(false, "@import " . $this->compileValue($importPath).";") :
+ $result;
+
+ break;
+ case "import_mixin":
+ list(,$importId) = $prop;
+ $import = $this->env->imports[$importId];
+ if ($import[0] === false) {
+ if (isset($import[1])) {
+ $out->lines[] = $import[1];
+ }
+ } else {
+ list(, $bottom, $parser, $importDir) = $import;
+ $this->compileImportedProps($bottom, $block, $out, $parser, $importDir);
+ }
+
+ break;
+ default:
+ $this->throwError("unknown op: {$prop[0]}\n");
+ }
+ }
+
+
+ /**
+ * Compiles a primitive value into a CSS property value.
+ *
+ * Values in lessphp are typed by being wrapped in arrays, their format is
+ * typically:
+ *
+ * array(type, contents [, additional_contents]*)
+ *
+ * The input is expected to be reduced. This function will not work on
+ * things like expressions and variables.
+ */
+ public function compileValue($value) {
+ switch ($value[0]) {
+ case 'list':
+ // [1] - delimiter
+ // [2] - array of values
+ return implode($value[1], array_map(array($this, 'compileValue'), $value[2]));
+ case 'raw_color':
+ if (!empty($this->formatter->compressColors)) {
+ return $this->compileValue($this->coerceColor($value));
+ }
+ return $value[1];
+ case 'keyword':
+ // [1] - the keyword
+ return $value[1];
+ case 'number':
+ list(, $num, $unit) = $value;
+ // [1] - the number
+ // [2] - the unit
+ if ($this->numberPrecision !== null) {
+ $num = round($num, $this->numberPrecision);
+ }
+ return $num . $unit;
+ case 'string':
+ // [1] - contents of string (includes quotes)
+ list(, $delim, $content) = $value;
+ foreach ($content as &$part) {
+ if (is_array($part)) {
+ $part = $this->compileValue($part);
+ }
+ }
+ return $delim . implode($content) . $delim;
+ case 'color':
+ // [1] - red component (either number or a %)
+ // [2] - green component
+ // [3] - blue component
+ // [4] - optional alpha component
+ list(, $r, $g, $b) = $value;
+ $r = round($r);
+ $g = round($g);
+ $b = round($b);
+
+ if (count($value) == 5 && $value[4] != 1) { // rgba
+ return 'rgba('.$r.','.$g.','.$b.','.$value[4].')';
+ }
+
+ $h = sprintf("#%02x%02x%02x", $r, $g, $b);
+
+ if (!empty($this->formatter->compressColors)) {
+ // Converting hex color to short notation (e.g. #003399 to #039)
+ if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
+ $h = '#' . $h[1] . $h[3] . $h[5];
+ }
+ }
+
+ return $h;
+
+ case 'function':
+ list(, $name, $args) = $value;
+ return $name.'('.$this->compileValue($args).')';
+ default: // assumed to be unit
+ $this->throwError("unknown value type: $value[0]");
+ }
+ }
+
+ protected function lib_pow($args) {
+ list($base, $exp) = $this->assertArgs($args, 2, "pow");
+ return pow($this->assertNumber($base), $this->assertNumber($exp));
+ }
+
+ protected function lib_pi() {
+ return pi();
+ }
+
+ protected function lib_mod($args) {
+ list($a, $b) = $this->assertArgs($args, 2, "mod");
+ return $this->assertNumber($a) % $this->assertNumber($b);
+ }
+
+ protected function lib_tan($num) {
+ return tan($this->assertNumber($num));
+ }
+
+ protected function lib_sin($num) {
+ return sin($this->assertNumber($num));
+ }
+
+ protected function lib_cos($num) {
+ return cos($this->assertNumber($num));
+ }
+
+ protected function lib_atan($num) {
+ $num = atan($this->assertNumber($num));
+ return array("number", $num, "rad");
+ }
+
+ protected function lib_asin($num) {
+ $num = asin($this->assertNumber($num));
+ return array("number", $num, "rad");
+ }
+
+ protected function lib_acos($num) {
+ $num = acos($this->assertNumber($num));
+ return array("number", $num, "rad");
+ }
+
+ protected function lib_sqrt($num) {
+ return sqrt($this->assertNumber($num));
+ }
+
+ protected function lib_extract($value) {
+ list($list, $idx) = $this->assertArgs($value, 2, "extract");
+ $idx = $this->assertNumber($idx);
+ // 1 indexed
+ if ($list[0] == "list" && isset($list[2][$idx - 1])) {
+ return $list[2][$idx - 1];
+ }
+ }
+
+ protected function lib_isnumber($value) {
+ return $this->toBool($value[0] == "number");
+ }
+
+ protected function lib_isstring($value) {
+ return $this->toBool($value[0] == "string");
+ }
+
+ protected function lib_iscolor($value) {
+ return $this->toBool($this->coerceColor($value));
+ }
+
+ protected function lib_iskeyword($value) {
+ return $this->toBool($value[0] == "keyword");
+ }
+
+ protected function lib_ispixel($value) {
+ return $this->toBool($value[0] == "number" && $value[2] == "px");
+ }
+
+ protected function lib_ispercentage($value) {
+ return $this->toBool($value[0] == "number" && $value[2] == "%");
+ }
+
+ protected function lib_isem($value) {
+ return $this->toBool($value[0] == "number" && $value[2] == "em");
+ }
+
+ protected function lib_isrem($value) {
+ return $this->toBool($value[0] == "number" && $value[2] == "rem");
+ }
+
+ protected function lib_rgbahex($color) {
+ $color = $this->coerceColor($color);
+ if (is_null($color)) {
+ $this->throwError("color expected for rgbahex");
+ }
+
+ return sprintf("#%02x%02x%02x%02x",
+ isset($color[4]) ? $color[4] * 255 : 255,
+ $color[1],
+ $color[2],
+ $color[3]
+ );
+ }
+
+ protected function lib_argb($color){
+ return $this->lib_rgbahex($color);
+ }
+
+ /**
+ * Given an url, decide whether to output a regular link or the base64-encoded contents of the file
+ *
+ * @param array $value either an argument list (two strings) or a single string
+ * @return string formatted url(), either as a link or base64-encoded
+ */
+ protected function lib_data_uri($value) {
+ $mime = ($value[0] === 'list') ? $value[2][0][2] : null;
+ $url = ($value[0] === 'list') ? $value[2][1][2][0] : $value[2][0];
+
+ $fullpath = $this->findImport($url);
+
+ if ($fullpath && ($fsize = filesize($fullpath)) !== false) {
+ // IE8 can't handle data uris larger than 32KB
+ if ($fsize/1024 < 32) {
+ if (is_null($mime)) {
+ if (class_exists('finfo')) { // php 5.3+
+ $finfo = new finfo(FILEINFO_MIME);
+ $mime = explode('; ', $finfo->file($fullpath));
+ $mime = $mime[0];
+ } elseif (function_exists('mime_content_type')) { // PHP 5.2
+ $mime = mime_content_type($fullpath);
+ }
+ }
+
+ if (!is_null($mime)) // fallback if the mime type is still unknown
+ $url = sprintf('data:%s;base64,%s', $mime, base64_encode(file_get_contents($fullpath)));
+ }
+ }
+
+ return 'url("'.$url.'")';
+ }
+
+ // utility func to unquote a string
+ protected function lib_e($arg) {
+ switch ($arg[0]) {
+ case "list":
+ $items = $arg[2];
+ if (isset($items[0])) {
+ return $this->lib_e($items[0]);
+ }
+ $this->throwError("unrecognised input");
+ case "string":
+ $arg[1] = "";
+ return $arg;
+ case "keyword":
+ return $arg;
+ default:
+ return array("keyword", $this->compileValue($arg));
+ }
+ }
+
+ protected function lib__sprintf($args) {
+ if ($args[0] != "list") return $args;
+ $values = $args[2];
+ $string = array_shift($values);
+ $template = $this->compileValue($this->lib_e($string));
+
+ $i = 0;
+ if (preg_match_all('/%[dsa]/', $template, $m)) {
+ foreach ($m[0] as $match) {
+ $val = isset($values[$i]) ?
+ $this->reduce($values[$i]) : array('keyword', '');
+
+ // lessjs compat, renders fully expanded color, not raw color
+ if ($color = $this->coerceColor($val)) {
+ $val = $color;
+ }
+
+ $i++;
+ $rep = $this->compileValue($this->lib_e($val));
+ $template = preg_replace('/'.self::preg_quote($match).'/',
+ $rep, $template, 1);
+ }
+ }
+
+ $d = $string[0] == "string" ? $string[1] : '"';
+ return array("string", $d, array($template));
+ }
+
+ protected function lib_floor($arg) {
+ $value = $this->assertNumber($arg);
+ return array("number", floor($value), $arg[2]);
+ }
+
+ protected function lib_ceil($arg) {
+ $value = $this->assertNumber($arg);
+ return array("number", ceil($value), $arg[2]);
+ }
+
+ protected function lib_round($arg) {
+ if ($arg[0] != "list") {
+ $value = $this->assertNumber($arg);
+ return array("number", round($value), $arg[2]);
+ } else {
+ $value = $this->assertNumber($arg[2][0]);
+ $precision = $this->assertNumber($arg[2][1]);
+ return array("number", round($value, $precision), $arg[2][0][2]);
+ }
+ }
+
+ protected function lib_unit($arg) {
+ if ($arg[0] == "list") {
+ list($number, $newUnit) = $arg[2];
+ return array("number", $this->assertNumber($number),
+ $this->compileValue($this->lib_e($newUnit)));
+ } else {
+ return array("number", $this->assertNumber($arg), "");
+ }
+ }
+
+ /**
+ * Helper function to get arguments for color manipulation functions.
+ * takes a list that contains a color like thing and a percentage
+ */
+ public function colorArgs($args) {
+ if ($args[0] != 'list' || count($args[2]) < 2) {
+ return array(array('color', 0, 0, 0), 0);
+ }
+ list($color, $delta) = $args[2];
+ $color = $this->assertColor($color);
+ $delta = floatval($delta[1]);
+
+ return array($color, $delta);
+ }
+
+ protected function lib_darken($args) {
+ list($color, $delta) = $this->colorArgs($args);
+
+ $hsl = $this->toHSL($color);
+ $hsl[3] = $this->clamp($hsl[3] - $delta, 100);
+ return $this->toRGB($hsl);
+ }
+
+ protected function lib_lighten($args) {
+ list($color, $delta) = $this->colorArgs($args);
+
+ $hsl = $this->toHSL($color);
+ $hsl[3] = $this->clamp($hsl[3] + $delta, 100);
+ return $this->toRGB($hsl);
+ }
+
+ protected function lib_saturate($args) {
+ list($color, $delta) = $this->colorArgs($args);
+
+ $hsl = $this->toHSL($color);
+ $hsl[2] = $this->clamp($hsl[2] + $delta, 100);
+ return $this->toRGB($hsl);
+ }
+
+ protected function lib_desaturate($args) {
+ list($color, $delta) = $this->colorArgs($args);
+
+ $hsl = $this->toHSL($color);
+ $hsl[2] = $this->clamp($hsl[2] - $delta, 100);
+ return $this->toRGB($hsl);
+ }
+
+ protected function lib_spin($args) {
+ list($color, $delta) = $this->colorArgs($args);
+
+ $hsl = $this->toHSL($color);
+
+ $hsl[1] = $hsl[1] + $delta % 360;
+ if ($hsl[1] < 0) {
+ $hsl[1] += 360;
+ }
+
+ return $this->toRGB($hsl);
+ }
+
+ protected function lib_fadeout($args) {
+ list($color, $delta) = $this->colorArgs($args);
+ $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100);
+ return $color;
+ }
+
+ protected function lib_fadein($args) {
+ list($color, $delta) = $this->colorArgs($args);
+ $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100);
+ return $color;
+ }
+
+ protected function lib_hue($color) {
+ $hsl = $this->toHSL($this->assertColor($color));
+ return round($hsl[1]);
+ }
+
+ protected function lib_saturation($color) {
+ $hsl = $this->toHSL($this->assertColor($color));
+ return round($hsl[2]);
+ }
+
+ protected function lib_lightness($color) {
+ $hsl = $this->toHSL($this->assertColor($color));
+ return round($hsl[3]);
+ }
+
+ // get the alpha of a color
+ // defaults to 1 for non-colors or colors without an alpha
+ protected function lib_alpha($value) {
+ if (!is_null($color = $this->coerceColor($value))) {
+ return isset($color[4]) ? $color[4] : 1;
+ }
+ }
+
+ // set the alpha of the color
+ protected function lib_fade($args) {
+ list($color, $alpha) = $this->colorArgs($args);
+ $color[4] = $this->clamp($alpha / 100.0);
+ return $color;
+ }
+
+ protected function lib_percentage($arg) {
+ $num = $this->assertNumber($arg);
+ return array("number", $num*100, "%");
+ }
+
+ /**
+ * Mix color with white in variable proportion.
+ *
+ * It is the same as calling `mix(#ffffff, @color, @weight)`.
+ *
+ * tint(@color, [@weight: 50%]);
+ *
+ * http://lesscss.org/functions/#color-operations-tint
+ *
+ * @return array Color
+ */
+ protected function lib_tint($args) {
+ $white = ['color', 255, 255, 255];
+ if ($args[0] == 'color') {
+ return $this->lib_mix([ 'list', ',', [$white, $args] ]);
+ } elseif ($args[0] == "list" && count($args[2]) == 2) {
+ return $this->lib_mix([ $args[0], $args[1], [$white, $args[2][0], $args[2][1]] ]);
+ } else {
+ $this->throwError("tint expects (color, weight)");
+ }
+ }
+
+ /**
+ * Mix color with black in variable proportion.
+ *
+ * It is the same as calling `mix(#000000, @color, @weight)`
+ *
+ * shade(@color, [@weight: 50%]);
+ *
+ * http://lesscss.org/functions/#color-operations-shade
+ *
+ * @return array Color
+ */
+ protected function lib_shade($args) {
+ $black = ['color', 0, 0, 0];
+ if ($args[0] == 'color') {
+ return $this->lib_mix([ 'list', ',', [$black, $args] ]);
+ } elseif ($args[0] == "list" && count($args[2]) == 2) {
+ return $this->lib_mix([ $args[0], $args[1], [$black, $args[2][0], $args[2][1]] ]);
+ } else {
+ $this->throwError("shade expects (color, weight)");
+ }
+ }
+
+ // mixes two colors by weight
+ // mix(@color1, @color2, [@weight: 50%]);
+ // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method
+ protected function lib_mix($args) {
+ if ($args[0] != "list" || count($args[2]) < 2)
+ $this->throwError("mix expects (color1, color2, weight)");
+
+ list($first, $second) = $args[2];
+ $first = $this->assertColor($first);
+ $second = $this->assertColor($second);
+
+ $first_a = $this->lib_alpha($first);
+ $second_a = $this->lib_alpha($second);
+
+ if (isset($args[2][2])) {
+ $weight = $args[2][2][1] / 100.0;
+ } else {
+ $weight = 0.5;
+ }
+
+ $w = $weight * 2 - 1;
+ $a = $first_a - $second_a;
+
+ $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
+ $w2 = 1.0 - $w1;
+
+ $new = array('color',
+ $w1 * $first[1] + $w2 * $second[1],
+ $w1 * $first[2] + $w2 * $second[2],
+ $w1 * $first[3] + $w2 * $second[3],
+ );
+
+ if ($first_a != 1.0 || $second_a != 1.0) {
+ $new[] = $first_a * $weight + $second_a * ($weight - 1);
+ }
+
+ return $this->fixColor($new);
+ }
+
+ protected function lib_contrast($args) {
+ $darkColor = array('color', 0, 0, 0);
+ $lightColor = array('color', 255, 255, 255);
+ $threshold = 0.43;
+
+ if ( $args[0] == 'list' ) {
+ $inputColor = ( isset($args[2][0]) ) ? $this->assertColor($args[2][0]) : $lightColor;
+ $darkColor = ( isset($args[2][1]) ) ? $this->assertColor($args[2][1]) : $darkColor;
+ $lightColor = ( isset($args[2][2]) ) ? $this->assertColor($args[2][2]) : $lightColor;
+ $threshold = ( isset($args[2][3]) ) ? $this->assertNumber($args[2][3]) : $threshold;
+ }
+ else {
+ $inputColor = $this->assertColor($args);
+ }
+
+ $inputColor = $this->coerceColor($inputColor);
+ $darkColor = $this->coerceColor($darkColor);
+ $lightColor = $this->coerceColor($lightColor);
+
+ //Figure out which is actually light and dark!
+ if ( $this->toLuma($darkColor) > $this->toLuma($lightColor) ) {
+ $t = $lightColor;
+ $lightColor = $darkColor;
+ $darkColor = $t;
+ }
+
+ $inputColor_alpha = $this->lib_alpha($inputColor);
+ if ( ( $this->toLuma($inputColor) * $inputColor_alpha) < $threshold) {
+ return $lightColor;
+ }
+ return $darkColor;
+ }
+
+ private function toLuma($color) {
+ list(, $r, $g, $b) = $this->coerceColor($color);
+
+ $r = $r / 255;
+ $g = $g / 255;
+ $b = $b / 255;
+
+ $r = ($r <= 0.03928) ? $r / 12.92 : pow((($r + 0.055) / 1.055), 2.4);
+ $g = ($g <= 0.03928) ? $g / 12.92 : pow((($g + 0.055) / 1.055), 2.4);
+ $b = ($b <= 0.03928) ? $b / 12.92 : pow((($b + 0.055) / 1.055), 2.4);
+
+ return (0.2126 * $r) + (0.7152 * $g) + (0.0722 * $b);
+ }
+
+ protected function lib_luma($color) {
+ return array("number", round($this->toLuma($color) * 100, 8), "%");
+ }
+
+
+ public function assertColor($value, $error = "expected color value") {
+ $color = $this->coerceColor($value);
+ if (is_null($color)) $this->throwError($error);
+ return $color;
+ }
+
+ public function assertNumber($value, $error = "expecting number") {
+ if ($value[0] == "number") return $value[1];
+ $this->throwError($error);
+ }
+
+ public function assertArgs($value, $expectedArgs, $name="") {
+ if ($expectedArgs == 1) {
+ return $value;
+ } else {
+ if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list");
+ $values = $value[2];
+ $numValues = count($values);
+ if ($expectedArgs != $numValues) {
+ if ($name) {
+ $name = $name . ": ";
+ }
+
+ $this->throwError("${name}expecting $expectedArgs arguments, got $numValues");
+ }
+
+ return $values;
+ }
+ }
+
+ protected function toHSL($color) {
+ if ($color[0] === 'hsl') {
+ return $color;
+ }
+
+ $r = $color[1] / 255;
+ $g = $color[2] / 255;
+ $b = $color[3] / 255;
+
+ $min = min($r, $g, $b);
+ $max = max($r, $g, $b);
+
+ $L = ($min + $max) / 2;
+ if ($min == $max) {
+ $S = $H = 0;
+ } else {
+ if ($L < 0.5) {
+ $S = ($max - $min) / ($max + $min);
+ } else {
+ $S = ($max - $min) / (2.0 - $max - $min);
+ }
+ if ($r == $max) {
+ $H = ($g - $b) / ($max - $min);
+ } elseif ($g == $max) {
+ $H = 2.0 + ($b - $r) / ($max - $min);
+ } elseif ($b == $max) {
+ $H = 4.0 + ($r - $g) / ($max - $min);
+ }
+
+ }
+
+ $out = array('hsl',
+ ($H < 0 ? $H + 6 : $H)*60,
+ $S * 100,
+ $L * 100,
+ );
+
+ if (count($color) > 4) {
+ // copy alpha
+ $out[] = $color[4];
+ }
+ return $out;
+ }
+
+ protected function toRGB_helper($comp, $temp1, $temp2) {
+ if ($comp < 0) {
+ $comp += 1.0;
+ } elseif ($comp > 1) {
+ $comp -= 1.0;
+ }
+
+ if (6 * $comp < 1) {
+ return $temp1 + ($temp2 - $temp1) * 6 * $comp;
+ }
+ if (2 * $comp < 1) {
+ return $temp2;
+ }
+ if (3 * $comp < 2) {
+ return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6;
+ }
+
+ return $temp1;
+ }
+
+ /**
+ * Converts a hsl array into a color value in rgb.
+ * Expects H to be in range of 0 to 360, S and L in 0 to 100
+ */
+ protected function toRGB($color) {
+ if ($color[0] === 'color') {
+ return $color;
+ }
+
+ $H = $color[1] / 360;
+ $S = $color[2] / 100;
+ $L = $color[3] / 100;
+
+ if ($S == 0) {
+ $r = $g = $b = $L;
+ } else {
+ $temp2 = $L < 0.5 ?
+ $L * (1.0 + $S) :
+ $L + $S - $L * $S;
+
+ $temp1 = 2.0 * $L - $temp2;
+
+ $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2);
+ $g = $this->toRGB_helper($H, $temp1, $temp2);
+ $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2);
+ }
+
+ // $out = array('color', round($r*255), round($g*255), round($b*255));
+ $out = array('color', $r*255, $g*255, $b*255);
+ if (count($color) > 4) {
+ // copy alpha
+ $out[] = $color[4];
+ }
+ return $out;
+ }
+
+ protected function clamp($v, $max = 1, $min = 0) {
+ return min($max, max($min, $v));
+ }
+
+ /**
+ * Convert the rgb, rgba, hsl color literals of function type
+ * as returned by the parser into values of color type.
+ */
+ protected function funcToColor($func) {
+ $fname = $func[1];
+ if ($func[2][0] != 'list') {
+ // need a list of arguments
+ return false;
+ }
+ $rawComponents = $func[2][2];
+
+ if ($fname == 'hsl' || $fname == 'hsla') {
+ $hsl = array('hsl');
+ $i = 0;
+ foreach ($rawComponents as $c) {
+ $val = $this->reduce($c);
+ $val = isset($val[1]) ? floatval($val[1]) : 0;
+
+ if ($i == 0) {
+ $clamp = 360;
+ } elseif ($i < 3) {
+ $clamp = 100;
+ } else {
+ $clamp = 1;
+ }
+
+ $hsl[] = $this->clamp($val, $clamp);
+ $i++;
+ }
+
+ while (count($hsl) < 4) {
+ $hsl[] = 0;
+ }
+ return $this->toRGB($hsl);
+
+ } elseif ($fname == 'rgb' || $fname == 'rgba') {
+ $components = array();
+ $i = 1;
+ foreach ($rawComponents as $c) {
+ $c = $this->reduce($c);
+ if ($i < 4) {
+ if ($c[0] == "number" && $c[2] == "%") {
+ $components[] = 255 * ($c[1] / 100);
+ } else {
+ $components[] = floatval($c[1]);
+ }
+ } elseif ($i == 4) {
+ if ($c[0] == "number" && $c[2] == "%") {
+ $components[] = 1.0 * ($c[1] / 100);
+ } else {
+ $components[] = floatval($c[1]);
+ }
+ } else break;
+
+ $i++;
+ }
+ while (count($components) < 3) {
+ $components[] = 0;
+ }
+ array_unshift($components, 'color');
+ return $this->fixColor($components);
+ }
+
+ return false;
+ }
+
+ protected function reduce($value, $forExpression = false) {
+ switch ($value[0]) {
+ case "interpolate":
+ $reduced = $this->reduce($value[1]);
+ $var = $this->compileValue($reduced);
+ $res = $this->reduce(array("variable", $this->vPrefix . $var));
+
+ if ($res[0] == "raw_color") {
+ $res = $this->coerceColor($res);
+ }
+
+ if (empty($value[2])) $res = $this->lib_e($res);
+
+ return $res;
+ case "variable":
+ $key = $value[1];
+ if (is_array($key)) {
+ $key = $this->reduce($key);
+ $key = $this->vPrefix . $this->compileValue($this->lib_e($key));
+ }
+
+ $seen =& $this->env->seenNames;
+
+ if (!empty($seen[$key])) {
+ $this->throwError("infinite loop detected: $key");
+ }
+
+ $seen[$key] = true;
+ $out = $this->reduce($this->get($key));
+ $seen[$key] = false;
+ return $out;
+ case "list":
+ foreach ($value[2] as &$item) {
+ $item = $this->reduce($item, $forExpression);
+ }
+ return $value;
+ case "expression":
+ return $this->evaluate($value);
+ case "string":
+ foreach ($value[2] as &$part) {
+ if (is_array($part)) {
+ $strip = $part[0] == "variable";
+ $part = $this->reduce($part);
+ if ($strip) $part = $this->lib_e($part);
+ }
+ }
+ return $value;
+ case "escape":
+ list(,$inner) = $value;
+ return $this->lib_e($this->reduce($inner));
+ case "function":
+ $color = $this->funcToColor($value);
+ if ($color) return $color;
+
+ list(, $name, $args) = $value;
+ if ($name == "%") $name = "_sprintf";
+
+ $f = isset($this->libFunctions[$name]) ?
+ $this->libFunctions[$name] : array($this, 'lib_'.str_replace('-', '_', $name));
+
+ if (is_callable($f)) {
+ if ($args[0] == 'list')
+ $args = self::compressList($args[2], $args[1]);
+
+ $ret = call_user_func($f, $this->reduce($args, true), $this);
+
+ if (is_null($ret)) {
+ return array("string", "", array(
+ $name, "(", $args, ")"
+ ));
+ }
+
+ // convert to a typed value if the result is a php primitive
+ if (is_numeric($ret)) {
+ $ret = array('number', $ret, "");
+ } elseif (!is_array($ret)) {
+ $ret = array('keyword', $ret);
+ }
+
+ return $ret;
+ }
+
+ // plain function, reduce args
+ $value[2] = $this->reduce($value[2]);
+ return $value;
+ case "unary":
+ list(, $op, $exp) = $value;
+ $exp = $this->reduce($exp);
+
+ if ($exp[0] == "number") {
+ switch ($op) {
+ case "+":
+ return $exp;
+ case "-":
+ $exp[1] *= -1;
+ return $exp;
+ }
+ }
+ return array("string", "", array($op, $exp));
+ }
+
+ if ($forExpression) {
+ switch ($value[0]) {
+ case "keyword":
+ if ($color = $this->coerceColor($value)) {
+ return $color;
+ }
+ break;
+ case "raw_color":
+ return $this->coerceColor($value);
+ }
+ }
+
+ return $value;
+ }
+
+
+ // coerce a value for use in color operation
+ protected function coerceColor($value) {
+ switch ($value[0]) {
+ case 'color': return $value;
+ case 'raw_color':
+ $c = array("color", 0, 0, 0);
+ $colorStr = substr($value[1], 1);
+ $num = hexdec($colorStr);
+ $width = strlen($colorStr) == 3 ? 16 : 256;
+
+ for ($i = 3; $i > 0; $i--) { // 3 2 1
+ $t = $num % $width;
+ $num /= $width;
+
+ $c[$i] = $t * (256/$width) + $t * floor(16/$width);
+ }
+
+ return $c;
+ case 'keyword':
+ $name = $value[1];
+ if (isset(self::$cssColors[$name])) {
+ $rgba = explode(',', self::$cssColors[$name]);
+
+ if (isset($rgba[3])) {
+ return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]);
+ }
+ return array('color', $rgba[0], $rgba[1], $rgba[2]);
+ }
+ return null;
+ }
+ }
+
+ // make something string like into a string
+ protected function coerceString($value) {
+ switch ($value[0]) {
+ case "string":
+ return $value;
+ case "keyword":
+ return array("string", "", array($value[1]));
+ }
+ return null;
+ }
+
+ // turn list of length 1 into value type
+ protected function flattenList($value) {
+ if ($value[0] == "list" && count($value[2]) == 1) {
+ return $this->flattenList($value[2][0]);
+ }
+ return $value;
+ }
+
+ public function toBool($a) {
+ return $a ? self::$TRUE : self::$FALSE;
+ }
+
+ // evaluate an expression
+ protected function evaluate($exp) {
+ list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
+
+ $left = $this->reduce($left, true);
+ $right = $this->reduce($right, true);
+
+ if ($leftColor = $this->coerceColor($left)) {
+ $left = $leftColor;
+ }
+
+ if ($rightColor = $this->coerceColor($right)) {
+ $right = $rightColor;
+ }
+
+ $ltype = $left[0];
+ $rtype = $right[0];
+
+ // operators that work on all types
+ if ($op == "and") {
+ return $this->toBool($left == self::$TRUE && $right == self::$TRUE);
+ }
+
+ if ($op == "=") {
+ return $this->toBool($this->eq($left, $right) );
+ }
+
+ if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right))) {
+ return $str;
+ }
+
+ // type based operators
+ $fname = "op_${ltype}_${rtype}";
+ if (is_callable(array($this, $fname))) {
+ $out = $this->$fname($op, $left, $right);
+ if (!is_null($out)) return $out;
+ }
+
+ // make the expression look it did before being parsed
+ $paddedOp = $op;
+ if ($whiteBefore) {
+ $paddedOp = " " . $paddedOp;
+ }
+ if ($whiteAfter) {
+ $paddedOp .= " ";
+ }
+
+ return array("string", "", array($left, $paddedOp, $right));
+ }
+
+ protected function stringConcatenate($left, $right) {
+ if ($strLeft = $this->coerceString($left)) {
+ if ($right[0] == "string") {
+ $right[1] = "";
+ }
+ $strLeft[2][] = $right;
+ return $strLeft;
+ }
+
+ if ($strRight = $this->coerceString($right)) {
+ array_unshift($strRight[2], $left);
+ return $strRight;
+ }
+ }
+
+
+ // make sure a color's components don't go out of bounds
+ protected function fixColor($c) {
+ foreach (range(1, 3) as $i) {
+ if ($c[$i] < 0) $c[$i] = 0;
+ if ($c[$i] > 255) $c[$i] = 255;
+ }
+
+ return $c;
+ }
+
+ protected function op_number_color($op, $lft, $rgt) {
+ if ($op == '+' || $op == '*') {
+ return $this->op_color_number($op, $rgt, $lft);
+ }
+ }
+
+ protected function op_color_number($op, $lft, $rgt) {
+ if ($rgt[0] == '%') $rgt[1] /= 100;
+
+ return $this->op_color_color($op, $lft,
+ array_fill(1, count($lft) - 1, $rgt[1]));
+ }
+
+ protected function op_color_color($op, $left, $right) {
+ $out = array('color');
+ $max = count($left) > count($right) ? count($left) : count($right);
+ foreach (range(1, $max - 1) as $i) {
+ $lval = isset($left[$i]) ? $left[$i] : 0;
+ $rval = isset($right[$i]) ? $right[$i] : 0;
+ switch ($op) {
+ case '+':
+ $out[] = $lval + $rval;
+ break;
+ case '-':
+ $out[] = $lval - $rval;
+ break;
+ case '*':
+ $out[] = $lval * $rval;
+ break;
+ case '%':
+ $out[] = $lval % $rval;
+ break;
+ case '/':
+ if ($rval == 0) {
+ $this->throwError("evaluate error: can't divide by zero");
+ }
+ $out[] = $lval / $rval;
+ break;
+ default:
+ $this->throwError('evaluate error: color op number failed on op '.$op);
+ }
+ }
+ return $this->fixColor($out);
+ }
+
+ public function lib_red($color){
+ $color = $this->coerceColor($color);
+ if (is_null($color)) {
+ $this->throwError('color expected for red()');
+ }
+
+ return $color[1];
+ }
+
+ public function lib_green($color){
+ $color = $this->coerceColor($color);
+ if (is_null($color)) {
+ $this->throwError('color expected for green()');
+ }
+
+ return $color[2];
+ }
+
+ public function lib_blue($color){
+ $color = $this->coerceColor($color);
+ if (is_null($color)) {
+ $this->throwError('color expected for blue()');
+ }
+
+ return $color[3];
+ }
+
+
+ // operator on two numbers
+ protected function op_number_number($op, $left, $right) {
+ $unit = empty($left[2]) ? $right[2] : $left[2];
+
+ $value = 0;
+ switch ($op) {
+ case '+':
+ $value = $left[1] + $right[1];
+ break;
+ case '*':
+ $value = $left[1] * $right[1];
+ break;
+ case '-':
+ $value = $left[1] - $right[1];
+ break;
+ case '%':
+ $value = $left[1] % $right[1];
+ break;
+ case '/':
+ if ($right[1] == 0) $this->throwError('parse error: divide by zero');
+ $value = $left[1] / $right[1];
+ break;
+ case '<':
+ return $this->toBool($left[1] < $right[1]);
+ case '>':
+ return $this->toBool($left[1] > $right[1]);
+ case '>=':
+ return $this->toBool($left[1] >= $right[1]);
+ case '=<':
+ return $this->toBool($left[1] <= $right[1]);
+ default:
+ $this->throwError('parse error: unknown number operator: '.$op);
+ }
+
+ return array("number", $value, $unit);
+ }
+
+
+ /* environment functions */
+
+ protected function makeOutputBlock($type, $selectors = null) {
+ $b = new stdclass;
+ $b->lines = array();
+ $b->children = array();
+ $b->selectors = $selectors;
+ $b->type = $type;
+ $b->parent = $this->scope;
+ return $b;
+ }
+
+ // the state of execution
+ protected function pushEnv($block = null) {
+ $e = new stdclass;
+ $e->parent = $this->env;
+ $e->store = array();
+ $e->block = $block;
+
+ $this->env = $e;
+ return $e;
+ }
+
+ // pop something off the stack
+ protected function popEnv() {
+ $old = $this->env;
+ $this->env = $this->env->parent;
+ return $old;
+ }
+
+ // set something in the current env
+ protected function set($name, $value) {
+ $this->env->store[$name] = $value;
+ }
+
+
+ // get the highest occurrence entry for a name
+ protected function get($name) {
+ $current = $this->env;
+
+ $isArguments = $name == $this->vPrefix . 'arguments';
+ while ($current) {
+ if ($isArguments && isset($current->arguments)) {
+ return array('list', ' ', $current->arguments);
+ }
+
+ if (isset($current->store[$name])) {
+ return $current->store[$name];
+ }
+
+ $current = isset($current->storeParent) ?
+ $current->storeParent :
+ $current->parent;
+ }
+
+ $this->throwError("variable $name is undefined");
+ }
+
+ // inject array of unparsed strings into environment as variables
+ protected function injectVariables($args) {
+ $this->pushEnv();
+ $parser = new lessc_parser($this, __METHOD__);
+ foreach ($args as $name => $strValue) {
+ if ($name{0} !== '@') {
+ $name = '@' . $name;
+ }
+ $parser->count = 0;
+ $parser->buffer = (string)$strValue;
+ if (!$parser->propertyValue($value)) {
+ throw new Exception("failed to parse passed in variable $name: $strValue");
+ }
+
+ $this->set($name, $value);
+ }
+ }
+
+ /**
+ * Initialize any static state, can initialize parser for a file
+ * $opts isn't used yet
+ */
+ public function __construct($fname = null) {
+ if ($fname !== null) {
+ // used for deprecated parse method
+ $this->_parseFile = $fname;
+ }
+ }
+
+ public function compile($string, $name = null) {
+ $locale = setlocale(LC_NUMERIC, 0);
+ setlocale(LC_NUMERIC, "C");
+
+ $this->parser = $this->makeParser($name);
+ $root = $this->parser->parse($string);
+
+ $this->env = null;
+ $this->scope = null;
+
+ $this->formatter = $this->newFormatter();
+
+ if (!empty($this->registeredVars)) {
+ $this->injectVariables($this->registeredVars);
+ }
+
+ $this->sourceParser = $this->parser; // used for error messages
+ $this->compileBlock($root);
+
+ ob_start();
+ $this->formatter->block($this->scope);
+ $out = ob_get_clean();
+ setlocale(LC_NUMERIC, $locale);
+ return $out;
+ }
+
+ public function compileFile($fname, $outFname = null) {
+ if (!is_readable($fname)) {
+ throw new Exception('load error: failed to find '.$fname);
+ }
+
+ $pi = pathinfo($fname);
+
+ $oldImport = $this->importDir;
+
+ $this->importDir = (array)$this->importDir;
+ $this->importDir[] = $pi['dirname'].'/';
+
+ $this->addParsedFile($fname);
+
+ $out = $this->compile(file_get_contents($fname), $fname);
+
+ $this->importDir = $oldImport;
+
+ if ($outFname !== null) {
+ return file_put_contents($outFname, $out);
+ }
+
+ return $out;
+ }
+
+ // compile only if changed input has changed or output doesn't exist
+ public function checkedCompile($in, $out) {
+ if (!is_file($out) || filemtime($in) > filemtime($out)) {
+ $this->compileFile($in, $out);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Execute lessphp on a .less file or a lessphp cache structure
+ *
+ * The lessphp cache structure contains information about a specific
+ * less file having been parsed. It can be used as a hint for future
+ * calls to determine whether or not a rebuild is required.
+ *
+ * The cache structure contains two important keys that may be used
+ * externally:
+ *
+ * compiled: The final compiled CSS
+ * updated: The time (in seconds) the CSS was last compiled
+ *
+ * The cache structure is a plain-ol' PHP associative array and can
+ * be serialized and unserialized without a hitch.
+ *
+ * @param mixed $in Input
+ * @param bool $force Force rebuild?
+ * @return array lessphp cache structure
+ */
+ public function cachedCompile($in, $force = false) {
+ // assume no root
+ $root = null;
+
+ if (is_string($in)) {
+ $root = $in;
+ } elseif (is_array($in) && isset($in['root'])) {
+ if ($force || !isset($in['files'])) {
+ // If we are forcing a recompile or if for some reason the
+ // structure does not contain any file information we should
+ // specify the root to trigger a rebuild.
+ $root = $in['root'];
+ } elseif (isset($in['files']) && is_array($in['files'])) {
+ foreach ($in['files'] as $fname => $ftime) {
+ if (!file_exists($fname) || filemtime($fname) > $ftime) {
+ // One of the files we knew about previously has changed
+ // so we should look at our incoming root again.
+ $root = $in['root'];
+ break;
+ }
+ }
+ }
+ } else {
+ // TODO: Throw an exception? We got neither a string nor something
+ // that looks like a compatible lessphp cache structure.
+ return null;
+ }
+
+ if ($root !== null) {
+ // If we have a root value which means we should rebuild.
+ $out = array();
+ $out['root'] = $root;
+ $out['compiled'] = $this->compileFile($root);
+ $out['files'] = $this->allParsedFiles();
+ $out['updated'] = time();
+ return $out;
+ } else {
+ // No changes, pass back the structure
+ // we were given initially.
+ return $in;
+ }
+
+ }
+
+ // parse and compile buffer
+ // This is deprecated
+ public function parse($str = null, $initialVariables = null) {
+ if (is_array($str)) {
+ $initialVariables = $str;
+ $str = null;
+ }
+
+ $oldVars = $this->registeredVars;
+ if ($initialVariables !== null) {
+ $this->setVariables($initialVariables);
+ }
+
+ if ($str == null) {
+ if (empty($this->_parseFile)) {
+ throw new exception("nothing to parse");
+ }
+
+ $out = $this->compileFile($this->_parseFile);
+ } else {
+ $out = $this->compile($str);
+ }
+
+ $this->registeredVars = $oldVars;
+ return $out;
+ }
+
+ protected function makeParser($name) {
+ $parser = new lessc_parser($this, $name);
+ $parser->writeComments = $this->preserveComments;
+
+ return $parser;
+ }
+
+ public function setFormatter($name) {
+ $this->formatterName = $name;
+ }
+
+ protected function newFormatter() {
+ $className = "lessc_formatter_lessjs";
+ if (!empty($this->formatterName)) {
+ if (!is_string($this->formatterName))
+ return $this->formatterName;
+ $className = "lessc_formatter_$this->formatterName";
+ }
+
+ return new $className;
+ }
+
+ public function setPreserveComments($preserve) {
+ $this->preserveComments = $preserve;
+ }
+
+ public function registerFunction($name, $func) {
+ $this->libFunctions[$name] = $func;
+ }
+
+ public function unregisterFunction($name) {
+ unset($this->libFunctions[$name]);
+ }
+
+ public function setVariables($variables) {
+ $this->registeredVars = array_merge($this->registeredVars, $variables);
+ }
+
+ public function unsetVariable($name) {
+ unset($this->registeredVars[$name]);
+ }
+
+ public function setImportDir($dirs) {
+ $this->importDir = (array)$dirs;
+ }
+
+ public function addImportDir($dir) {
+ $this->importDir = (array)$this->importDir;
+ $this->importDir[] = $dir;
+ }
+
+ public function allParsedFiles() {
+ return $this->allParsedFiles;
+ }
+
+ public function addParsedFile($file) {
+ $this->allParsedFiles[realpath($file)] = filemtime($file);
+ }
+
+ /**
+ * Uses the current value of $this->count to show line and line number
+ */
+ public function throwError($msg = null) {
+ if ($this->sourceLoc >= 0) {
+ $this->sourceParser->throwError($msg, $this->sourceLoc);
+ }
+ throw new exception($msg);
+ }
+
+ // compile file $in to file $out if $in is newer than $out
+ // returns true when it compiles, false otherwise
+ public static function ccompile($in, $out, $less = null) {
+ if ($less === null) {
+ $less = new self;
+ }
+ return $less->checkedCompile($in, $out);
+ }
+
+ public static function cexecute($in, $force = false, $less = null) {
+ if ($less === null) {
+ $less = new self;
+ }
+ return $less->cachedCompile($in, $force);
+ }
+
+ static protected $cssColors = array(
+ 'aliceblue' => '240,248,255',
+ 'antiquewhite' => '250,235,215',
+ 'aqua' => '0,255,255',
+ 'aquamarine' => '127,255,212',
+ 'azure' => '240,255,255',
+ 'beige' => '245,245,220',
+ 'bisque' => '255,228,196',
+ 'black' => '0,0,0',
+ 'blanchedalmond' => '255,235,205',
+ 'blue' => '0,0,255',
+ 'blueviolet' => '138,43,226',
+ 'brown' => '165,42,42',
+ 'burlywood' => '222,184,135',
+ 'cadetblue' => '95,158,160',
+ 'chartreuse' => '127,255,0',
+ 'chocolate' => '210,105,30',
+ 'coral' => '255,127,80',
+ 'cornflowerblue' => '100,149,237',
+ 'cornsilk' => '255,248,220',
+ 'crimson' => '220,20,60',
+ 'cyan' => '0,255,255',
+ 'darkblue' => '0,0,139',
+ 'darkcyan' => '0,139,139',
+ 'darkgoldenrod' => '184,134,11',
+ 'darkgray' => '169,169,169',
+ 'darkgreen' => '0,100,0',
+ 'darkgrey' => '169,169,169',
+ 'darkkhaki' => '189,183,107',
+ 'darkmagenta' => '139,0,139',
+ 'darkolivegreen' => '85,107,47',
+ 'darkorange' => '255,140,0',
+ 'darkorchid' => '153,50,204',
+ 'darkred' => '139,0,0',
+ 'darksalmon' => '233,150,122',
+ 'darkseagreen' => '143,188,143',
+ 'darkslateblue' => '72,61,139',
+ 'darkslategray' => '47,79,79',
+ 'darkslategrey' => '47,79,79',
+ 'darkturquoise' => '0,206,209',
+ 'darkviolet' => '148,0,211',
+ 'deeppink' => '255,20,147',
+ 'deepskyblue' => '0,191,255',
+ 'dimgray' => '105,105,105',
+ 'dimgrey' => '105,105,105',
+ 'dodgerblue' => '30,144,255',
+ 'firebrick' => '178,34,34',
+ 'floralwhite' => '255,250,240',
+ 'forestgreen' => '34,139,34',
+ 'fuchsia' => '255,0,255',
+ 'gainsboro' => '220,220,220',
+ 'ghostwhite' => '248,248,255',
+ 'gold' => '255,215,0',
+ 'goldenrod' => '218,165,32',
+ 'gray' => '128,128,128',
+ 'green' => '0,128,0',
+ 'greenyellow' => '173,255,47',
+ 'grey' => '128,128,128',
+ 'honeydew' => '240,255,240',
+ 'hotpink' => '255,105,180',
+ 'indianred' => '205,92,92',
+ 'indigo' => '75,0,130',
+ 'ivory' => '255,255,240',
+ 'khaki' => '240,230,140',
+ 'lavender' => '230,230,250',
+ 'lavenderblush' => '255,240,245',
+ 'lawngreen' => '124,252,0',
+ 'lemonchiffon' => '255,250,205',
+ 'lightblue' => '173,216,230',
+ 'lightcoral' => '240,128,128',
+ 'lightcyan' => '224,255,255',
+ 'lightgoldenrodyellow' => '250,250,210',
+ 'lightgray' => '211,211,211',
+ 'lightgreen' => '144,238,144',
+ 'lightgrey' => '211,211,211',
+ 'lightpink' => '255,182,193',
+ 'lightsalmon' => '255,160,122',
+ 'lightseagreen' => '32,178,170',
+ 'lightskyblue' => '135,206,250',
+ 'lightslategray' => '119,136,153',
+ 'lightslategrey' => '119,136,153',
+ 'lightsteelblue' => '176,196,222',
+ 'lightyellow' => '255,255,224',
+ 'lime' => '0,255,0',
+ 'limegreen' => '50,205,50',
+ 'linen' => '250,240,230',
+ 'magenta' => '255,0,255',
+ 'maroon' => '128,0,0',
+ 'mediumaquamarine' => '102,205,170',
+ 'mediumblue' => '0,0,205',
+ 'mediumorchid' => '186,85,211',
+ 'mediumpurple' => '147,112,219',
+ 'mediumseagreen' => '60,179,113',
+ 'mediumslateblue' => '123,104,238',
+ 'mediumspringgreen' => '0,250,154',
+ 'mediumturquoise' => '72,209,204',
+ 'mediumvioletred' => '199,21,133',
+ 'midnightblue' => '25,25,112',
+ 'mintcream' => '245,255,250',
+ 'mistyrose' => '255,228,225',
+ 'moccasin' => '255,228,181',
+ 'navajowhite' => '255,222,173',
+ 'navy' => '0,0,128',
+ 'oldlace' => '253,245,230',
+ 'olive' => '128,128,0',
+ 'olivedrab' => '107,142,35',
+ 'orange' => '255,165,0',
+ 'orangered' => '255,69,0',
+ 'orchid' => '218,112,214',
+ 'palegoldenrod' => '238,232,170',
+ 'palegreen' => '152,251,152',
+ 'paleturquoise' => '175,238,238',
+ 'palevioletred' => '219,112,147',
+ 'papayawhip' => '255,239,213',
+ 'peachpuff' => '255,218,185',
+ 'peru' => '205,133,63',
+ 'pink' => '255,192,203',
+ 'plum' => '221,160,221',
+ 'powderblue' => '176,224,230',
+ 'purple' => '128,0,128',
+ 'red' => '255,0,0',
+ 'rosybrown' => '188,143,143',
+ 'royalblue' => '65,105,225',
+ 'saddlebrown' => '139,69,19',
+ 'salmon' => '250,128,114',
+ 'sandybrown' => '244,164,96',
+ 'seagreen' => '46,139,87',
+ 'seashell' => '255,245,238',
+ 'sienna' => '160,82,45',
+ 'silver' => '192,192,192',
+ 'skyblue' => '135,206,235',
+ 'slateblue' => '106,90,205',
+ 'slategray' => '112,128,144',
+ 'slategrey' => '112,128,144',
+ 'snow' => '255,250,250',
+ 'springgreen' => '0,255,127',
+ 'steelblue' => '70,130,180',
+ 'tan' => '210,180,140',
+ 'teal' => '0,128,128',
+ 'thistle' => '216,191,216',
+ 'tomato' => '255,99,71',
+ 'transparent' => '0,0,0,0',
+ 'turquoise' => '64,224,208',
+ 'violet' => '238,130,238',
+ 'wheat' => '245,222,179',
+ 'white' => '255,255,255',
+ 'whitesmoke' => '245,245,245',
+ 'yellow' => '255,255,0',
+ 'yellowgreen' => '154,205,50'
+ );
+}
+
+// responsible for taking a string of LESS code and converting it into a
+// syntax tree
+class lessc_parser {
+ static protected $nextBlockId = 0; // used to uniquely identify blocks
+
+ static protected $precedence = array(
+ '=<' => 0,
+ '>=' => 0,
+ '=' => 0,
+ '<' => 0,
+ '>' => 0,
+
+ '+' => 1,
+ '-' => 1,
+ '*' => 2,
+ '/' => 2,
+ '%' => 2,
+ );
+
+ static protected $whitePattern;
+ static protected $commentMulti;
+
+ static protected $commentSingle = "//";
+ static protected $commentMultiLeft = "/*";
+ static protected $commentMultiRight = "*/";
+
+ // regex string to match any of the operators
+ static protected $operatorString;
+
+ // these properties will supress division unless it's inside parenthases
+ static protected $supressDivisionProps =
+ array('/border-radius$/i', '/^font$/i');
+
+ protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport");
+ protected $lineDirectives = array("charset");
+
+ /**
+ * if we are in parens we can be more liberal with whitespace around
+ * operators because it must evaluate to a single value and thus is less
+ * ambiguous.
+ *
+ * Consider:
+ * property1: 10 -5; // is two numbers, 10 and -5
+ * property2: (10 -5); // should evaluate to 5
+ */
+ protected $inParens = false;
+
+ // caches preg escaped literals
+ static protected $literalCache = array();
+
+ public function __construct($lessc, $sourceName = null) {
+ $this->eatWhiteDefault = true;
+ // reference to less needed for vPrefix, mPrefix, and parentSelector
+ $this->lessc = $lessc;
+
+ $this->sourceName = $sourceName; // name used for error messages
+
+ $this->writeComments = false;
+
+ if (!self::$operatorString) {
+ self::$operatorString =
+ '('.implode('|', array_map(array('lessc', 'preg_quote'),
+ array_keys(self::$precedence))).')';
+
+ $commentSingle = lessc::preg_quote(self::$commentSingle);
+ $commentMultiLeft = lessc::preg_quote(self::$commentMultiLeft);
+ $commentMultiRight = lessc::preg_quote(self::$commentMultiRight);
+
+ self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight;
+ self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais';
+ }
+ }
+
+ public function parse($buffer) {
+ $this->count = 0;
+ $this->line = 1;
+
+ $this->env = null; // block stack
+ $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer);
+ $this->pushSpecialBlock("root");
+ $this->eatWhiteDefault = true;
+ $this->seenComments = array();
+
+ // trim whitespace on head
+ // if (preg_match('/^\s+/', $this->buffer, $m)) {
+ // $this->line += substr_count($m[0], "\n");
+ // $this->buffer = ltrim($this->buffer);
+ // }
+ $this->whitespace();
+
+ // parse the entire file
+ while (false !== $this->parseChunk());
+
+ if ($this->count != strlen($this->buffer))
+ $this->throwError();
+
+ // TODO report where the block was opened
+ if ( !property_exists($this->env, 'parent') || !is_null($this->env->parent) )
+ throw new exception('parse error: unclosed block');
+
+ return $this->env;
+ }
+
+ /**
+ * Parse a single chunk off the head of the buffer and append it to the
+ * current parse environment.
+ * Returns false when the buffer is empty, or when there is an error.
+ *
+ * This function is called repeatedly until the entire document is
+ * parsed.
+ *
+ * This parser is most similar to a recursive descent parser. Single
+ * functions represent discrete grammatical rules for the language, and
+ * they are able to capture the text that represents those rules.
+ *
+ * Consider the function lessc::keyword(). (all parse functions are
+ * structured the same)
+ *
+ * The function takes a single reference argument. When calling the
+ * function it will attempt to match a keyword on the head of the buffer.
+ * If it is successful, it will place the keyword in the referenced
+ * argument, advance the position in the buffer, and return true. If it
+ * fails then it won't advance the buffer and it will return false.
+ *
+ * All of these parse functions are powered by lessc::match(), which behaves
+ * the same way, but takes a literal regular expression. Sometimes it is
+ * more convenient to use match instead of creating a new function.
+ *
+ * Because of the format of the functions, to parse an entire string of
+ * grammatical rules, you can chain them together using &&.
+ *
+ * But, if some of the rules in the chain succeed before one fails, then
+ * the buffer position will be left at an invalid state. In order to
+ * avoid this, lessc::seek() is used to remember and set buffer positions.
+ *
+ * Before parsing a chain, use $s = $this->seek() to remember the current
+ * position into $s. Then if a chain fails, use $this->seek($s) to
+ * go back where we started.
+ */
+ protected function parseChunk() {
+ if (empty($this->buffer)) return false;
+ $s = $this->seek();
+
+ if ($this->whitespace()) {
+ return true;
+ }
+
+ // setting a property
+ if ($this->keyword($key) && $this->assign() &&
+ $this->propertyValue($value, $key) && $this->end()
+ ) {
+ $this->append(array('assign', $key, $value), $s);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+
+ // look for special css blocks
+ if ($this->literal('@', false)) {
+ $this->count--;
+
+ // media
+ if ($this->literal('@media')) {
+ if (($this->mediaQueryList($mediaQueries) || true)
+ && $this->literal('{')
+ ) {
+ $media = $this->pushSpecialBlock("media");
+ $media->queries = is_null($mediaQueries) ? array() : $mediaQueries;
+ return true;
+ } else {
+ $this->seek($s);
+ return false;
+ }
+ }
+
+ if ($this->literal("@", false) && $this->keyword($dirName)) {
+ if ($this->isDirective($dirName, $this->blockDirectives)) {
+ if (($this->openString("{", $dirValue, null, array(";")) || true) &&
+ $this->literal("{")
+ ) {
+ $dir = $this->pushSpecialBlock("directive");
+ $dir->name = $dirName;
+ if (isset($dirValue)) $dir->value = $dirValue;
+ return true;
+ }
+ } elseif ($this->isDirective($dirName, $this->lineDirectives)) {
+ if ($this->propertyValue($dirValue) && $this->end()) {
+ $this->append(array("directive", $dirName, $dirValue));
+ return true;
+ }
+ }
+ }
+
+ $this->seek($s);
+ }
+
+ // setting a variable
+ if ($this->variable($var) && $this->assign() &&
+ $this->propertyValue($value) && $this->end()
+ ) {
+ $this->append(array('assign', $var, $value), $s);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ if ($this->import($importValue)) {
+ $this->append($importValue, $s);
+ return true;
+ }
+
+ // opening parametric mixin
+ if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) &&
+ ($this->guards($guards) || true) &&
+ $this->literal('{')
+ ) {
+ $block = $this->pushBlock($this->fixTags(array($tag)));
+ $block->args = $args;
+ $block->isVararg = $isVararg;
+ if (!empty($guards)) $block->guards = $guards;
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ // opening a simple block
+ if ($this->tags($tags) && $this->literal('{', false)) {
+ $tags = $this->fixTags($tags);
+ $this->pushBlock($tags);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ // closing a block
+ if ($this->literal('}', false)) {
+ try {
+ $block = $this->pop();
+ } catch (exception $e) {
+ $this->seek($s);
+ $this->throwError($e->getMessage());
+ }
+
+ $hidden = false;
+ if (is_null($block->type)) {
+ $hidden = true;
+ if (!isset($block->args)) {
+ foreach ($block->tags as $tag) {
+ if (!is_string($tag) || $tag{0} != $this->lessc->mPrefix) {
+ $hidden = false;
+ break;
+ }
+ }
+ }
+
+ foreach ($block->tags as $tag) {
+ if (is_string($tag)) {
+ $this->env->children[$tag][] = $block;
+ }
+ }
+ }
+
+ if (!$hidden) {
+ $this->append(array('block', $block), $s);
+ }
+
+ // this is done here so comments aren't bundled into he block that
+ // was just closed
+ $this->whitespace();
+ return true;
+ }
+
+ // mixin
+ if ($this->mixinTags($tags) &&
+ ($this->argumentDef($argv, $isVararg) || true) &&
+ ($this->keyword($suffix) || true) && $this->end()
+ ) {
+ $tags = $this->fixTags($tags);
+ $this->append(array('mixin', $tags, $argv, $suffix), $s);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ // spare ;
+ if ($this->literal(';')) return true;
+
+ return false; // got nothing, throw error
+ }
+
+ protected function isDirective($dirname, $directives) {
+ // TODO: cache pattern in parser
+ $pattern = implode("|",
+ array_map(array("lessc", "preg_quote"), $directives));
+ $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i';
+
+ return preg_match($pattern, $dirname);
+ }
+
+ protected function fixTags($tags) {
+ // move @ tags out of variable namespace
+ foreach ($tags as &$tag) {
+ if ($tag{0} == $this->lessc->vPrefix)
+ $tag[0] = $this->lessc->mPrefix;
+ }
+ return $tags;
+ }
+
+ // a list of expressions
+ protected function expressionList(&$exps) {
+ $values = array();
+
+ while ($this->expression($exp)) {
+ $values[] = $exp;
+ }
+
+ if (count($values) == 0) return false;
+
+ $exps = lessc::compressList($values, ' ');
+ return true;
+ }
+
+ /**
+ * Attempt to consume an expression.
+ * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
+ */
+ protected function expression(&$out) {
+ if ($this->value($lhs)) {
+ $out = $this->expHelper($lhs, 0);
+
+ // look for / shorthand
+ if (!empty($this->env->supressedDivision)) {
+ unset($this->env->supressedDivision);
+ $s = $this->seek();
+ if ($this->literal("/") && $this->value($rhs)) {
+ $out = array("list", "",
+ array($out, array("keyword", "/"), $rhs));
+ } else {
+ $this->seek($s);
+ }
+ }
+
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * recursively parse infix equation with $lhs at precedence $minP
+ */
+ protected function expHelper($lhs, $minP) {
+ $this->inExp = true;
+ $ss = $this->seek();
+
+ while (true) {
+ $whiteBefore = isset($this->buffer[$this->count - 1]) &&
+ ctype_space($this->buffer[$this->count - 1]);
+
+ // If there is whitespace before the operator, then we require
+ // whitespace after the operator for it to be an expression
+ $needWhite = $whiteBefore && !$this->inParens;
+
+ if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) {
+ if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) {
+ foreach (self::$supressDivisionProps as $pattern) {
+ if (preg_match($pattern, $this->env->currentProperty)) {
+ $this->env->supressedDivision = true;
+ break 2;
+ }
+ }
+ }
+
+
+ $whiteAfter = isset($this->buffer[$this->count - 1]) &&
+ ctype_space($this->buffer[$this->count - 1]);
+
+ if (!$this->value($rhs)) break;
+
+ // peek for next operator to see what to do with rhs
+ if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) {
+ $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
+ }
+
+ $lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter);
+ $ss = $this->seek();
+
+ continue;
+ }
+
+ break;
+ }
+
+ $this->seek($ss);
+
+ return $lhs;
+ }
+
+ // consume a list of values for a property
+ public function propertyValue(&$value, $keyName = null) {
+ $values = array();
+
+ if ($keyName !== null) $this->env->currentProperty = $keyName;
+
+ $s = null;
+ while ($this->expressionList($v)) {
+ $values[] = $v;
+ $s = $this->seek();
+ if (!$this->literal(',')) break;
+ }
+
+ if ($s) $this->seek($s);
+
+ if ($keyName !== null) unset($this->env->currentProperty);
+
+ if (count($values) == 0) return false;
+
+ $value = lessc::compressList($values, ', ');
+ return true;
+ }
+
+ protected function parenValue(&$out) {
+ $s = $this->seek();
+
+ // speed shortcut
+ if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") {
+ return false;
+ }
+
+ $inParens = $this->inParens;
+ if ($this->literal("(") &&
+ ($this->inParens = true) && $this->expression($exp) &&
+ $this->literal(")")
+ ) {
+ $out = $exp;
+ $this->inParens = $inParens;
+ return true;
+ } else {
+ $this->inParens = $inParens;
+ $this->seek($s);
+ }
+
+ return false;
+ }
+
+ // a single value
+ protected function value(&$value) {
+ $s = $this->seek();
+
+ // speed shortcut
+ if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") {
+ // negation
+ if ($this->literal("-", false) &&
+ (($this->variable($inner) && $inner = array("variable", $inner)) ||
+ $this->unit($inner) ||
+ $this->parenValue($inner))
+ ) {
+ $value = array("unary", "-", $inner);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+ }
+
+ if ($this->parenValue($value)) return true;
+ if ($this->unit($value)) return true;
+ if ($this->color($value)) return true;
+ if ($this->func($value)) return true;
+ if ($this->string($value)) return true;
+
+ if ($this->keyword($word)) {
+ $value = array('keyword', $word);
+ return true;
+ }
+
+ // try a variable
+ if ($this->variable($var)) {
+ $value = array('variable', $var);
+ return true;
+ }
+
+ // unquote string (should this work on any type?
+ if ($this->literal("~") && $this->string($str)) {
+ $value = array("escape", $str);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ // css hack: \0
+ if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
+ $value = array('keyword', '\\'.$m[1]);
+ return true;
+ } else {
+ $this->seek($s);
+ }
+
+ return false;
+ }
+
+ // an import statement
+ protected function import(&$out) {
+ if (!$this->literal('@import')) return false;
+
+ // @import "something.css" media;
+ // @import url("something.css") media;
+ // @import url(something.css) media;
+
+ if ($this->propertyValue($value)) {
+ $out = array("import", $value);
+ return true;
+ }
+ }
+
+ protected function mediaQueryList(&$out) {
+ if ($this->genericList($list, "mediaQuery", ",", false)) {
+ $out = $list[2];
+ return true;
+ }
+ return false;
+ }
+
+ protected function mediaQuery(&$out) {
+ $s = $this->seek();
+
+ $expressions = null;
+ $parts = array();
+
+ if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) {
+ $prop = array("mediaType");
+ if (isset($only)) $prop[] = "only";
+ if (isset($not)) $prop[] = "not";
+ $prop[] = $mediaType;
+ $parts[] = $prop;
+ } else {
+ $this->seek($s);
+ }
+
+
+ if (!empty($mediaType) && !$this->literal("and")) {
+ // ~
+ } else {
+ $this->genericList($expressions, "mediaExpression", "and", false);
+ if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]);
+ }
+
+ if (count($parts) == 0) {
+ $this->seek($s);
+ return false;
+ }
+
+ $out = $parts;
+ return true;
+ }
+
+ protected function mediaExpression(&$out) {
+ $s = $this->seek();
+ $value = null;
+ if ($this->literal("(") &&
+ $this->keyword($feature) &&
+ ($this->literal(":") && $this->expression($value) || true) &&
+ $this->literal(")")
+ ) {
+ $out = array("mediaExp", $feature);
+ if ($value) $out[] = $value;
+ return true;
+ } elseif ($this->variable($variable)) {
+ $out = array('variable', $variable);
+ return true;
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ // an unbounded string stopped by $end
+ protected function openString($end, &$out, $nestingOpen=null, $rejectStrs = null) {
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ $stop = array("'", '"', "@{", $end);
+ $stop = array_map(array("lessc", "preg_quote"), $stop);
+ // $stop[] = self::$commentMulti;
+
+ if (!is_null($rejectStrs)) {
+ $stop = array_merge($stop, $rejectStrs);
+ }
+
+ $patt = '(.*?)('.implode("|", $stop).')';
+
+ $nestingLevel = 0;
+
+ $content = array();
+ while ($this->match($patt, $m, false)) {
+ if (!empty($m[1])) {
+ $content[] = $m[1];
+ if ($nestingOpen) {
+ $nestingLevel += substr_count($m[1], $nestingOpen);
+ }
+ }
+
+ $tok = $m[2];
+
+ $this->count-= strlen($tok);
+ if ($tok == $end) {
+ if ($nestingLevel == 0) {
+ break;
+ } else {
+ $nestingLevel--;
+ }
+ }
+
+ if (($tok == "'" || $tok == '"') && $this->string($str)) {
+ $content[] = $str;
+ continue;
+ }
+
+ if ($tok == "@{" && $this->interpolation($inter)) {
+ $content[] = $inter;
+ continue;
+ }
+
+ if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) {
+ break;
+ }
+
+ $content[] = $tok;
+ $this->count+= strlen($tok);
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+
+ if (count($content) == 0) return false;
+
+ // trim the end
+ if (is_string(end($content))) {
+ $content[count($content) - 1] = rtrim(end($content));
+ }
+
+ $out = array("string", "", $content);
+ return true;
+ }
+
+ protected function string(&$out) {
+ $s = $this->seek();
+ if ($this->literal('"', false)) {
+ $delim = '"';
+ } elseif ($this->literal("'", false)) {
+ $delim = "'";
+ } else {
+ return false;
+ }
+
+ $content = array();
+
+ // look for either ending delim , escape, or string interpolation
+ $patt = '([^\n]*?)(@\{|\\\\|' .
+ lessc::preg_quote($delim).')';
+
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ while ($this->match($patt, $m, false)) {
+ $content[] = $m[1];
+ if ($m[2] == "@{") {
+ $this->count -= strlen($m[2]);
+ if ($this->interpolation($inter, false)) {
+ $content[] = $inter;
+ } else {
+ $this->count += strlen($m[2]);
+ $content[] = "@{"; // ignore it
+ }
+ } elseif ($m[2] == '\\') {
+ $content[] = $m[2];
+ if ($this->literal($delim, false)) {
+ $content[] = $delim;
+ }
+ } else {
+ $this->count -= strlen($delim);
+ break; // delim
+ }
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+
+ if ($this->literal($delim)) {
+ $out = array("string", $delim, $content);
+ return true;
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ protected function interpolation(&$out) {
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = true;
+
+ $s = $this->seek();
+ if ($this->literal("@{") &&
+ $this->openString("}", $interp, null, array("'", '"', ";")) &&
+ $this->literal("}", false)
+ ) {
+ $out = array("interpolate", $interp);
+ $this->eatWhiteDefault = $oldWhite;
+ if ($this->eatWhiteDefault) $this->whitespace();
+ return true;
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+ $this->seek($s);
+ return false;
+ }
+
+ protected function unit(&$unit) {
+ // speed shortcut
+ if (isset($this->buffer[$this->count])) {
+ $char = $this->buffer[$this->count];
+ if (!ctype_digit($char) && $char != ".") return false;
+ }
+
+ if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) {
+ $unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]);
+ return true;
+ }
+ return false;
+ }
+
+ // a # color
+ protected function color(&$out) {
+ if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) {
+ if (strlen($m[1]) > 7) {
+ $out = array("string", "", array($m[1]));
+ } else {
+ $out = array("raw_color", $m[1]);
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ // consume an argument definition list surrounded by ()
+ // each argument is a variable name with optional value
+ // or at the end a ... or a variable named followed by ...
+ // arguments are separated by , unless a ; is in the list, then ; is the
+ // delimiter.
+ protected function argumentDef(&$args, &$isVararg) {
+ $s = $this->seek();
+ if (!$this->literal('(')) {
+ return false;
+ }
+
+ $values = array();
+ $delim = ",";
+ $method = "expressionList";
+
+ $isVararg = false;
+ while (true) {
+ if ($this->literal("...")) {
+ $isVararg = true;
+ break;
+ }
+
+ if ($this->$method($value)) {
+ if ($value[0] == "variable") {
+ $arg = array("arg", $value[1]);
+ $ss = $this->seek();
+
+ if ($this->assign() && $this->$method($rhs)) {
+ $arg[] = $rhs;
+ } else {
+ $this->seek($ss);
+ if ($this->literal("...")) {
+ $arg[0] = "rest";
+ $isVararg = true;
+ }
+ }
+
+ $values[] = $arg;
+ if ($isVararg) {
+ break;
+ }
+ continue;
+ } else {
+ $values[] = array("lit", $value);
+ }
+ }
+
+
+ if (!$this->literal($delim)) {
+ if ($delim == "," && $this->literal(";")) {
+ // found new delim, convert existing args
+ $delim = ";";
+ $method = "propertyValue";
+
+ // transform arg list
+ if (isset($values[1])) { // 2 items
+ $newList = array();
+ foreach ($values as $i => $arg) {
+ switch ($arg[0]) {
+ case "arg":
+ if ($i) {
+ $this->throwError("Cannot mix ; and , as delimiter types");
+ }
+ $newList[] = $arg[2];
+ break;
+ case "lit":
+ $newList[] = $arg[1];
+ break;
+ case "rest":
+ $this->throwError("Unexpected rest before semicolon");
+ }
+ }
+
+ $newList = array("list", ", ", $newList);
+
+ switch ($values[0][0]) {
+ case "arg":
+ $newArg = array("arg", $values[0][1], $newList);
+ break;
+ case "lit":
+ $newArg = array("lit", $newList);
+ break;
+ }
+
+ } elseif ($values) { // 1 item
+ $newArg = $values[0];
+ }
+
+ if ($newArg) {
+ $values = array($newArg);
+ }
+ } else {
+ break;
+ }
+ }
+ }
+
+ if (!$this->literal(')')) {
+ $this->seek($s);
+ return false;
+ }
+
+ $args = $values;
+
+ return true;
+ }
+
+ // consume a list of tags
+ // this accepts a hanging delimiter
+ protected function tags(&$tags, $simple = false, $delim = ',') {
+ $tags = array();
+ while ($this->tag($tt, $simple)) {
+ $tags[] = $tt;
+ if (!$this->literal($delim)) break;
+ }
+ if (count($tags) == 0) return false;
+
+ return true;
+ }
+
+ // list of tags of specifying mixin path
+ // optionally separated by > (lazy, accepts extra >)
+ protected function mixinTags(&$tags) {
+ $tags = array();
+ while ($this->tag($tt, true)) {
+ $tags[] = $tt;
+ $this->literal(">");
+ }
+
+ if (!$tags) {
+ return false;
+ }
+
+ return true;
+ }
+
+ // a bracketed value (contained within in a tag definition)
+ protected function tagBracket(&$parts, &$hasExpression) {
+ // speed shortcut
+ if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") {
+ return false;
+ }
+
+ $s = $this->seek();
+
+ $hasInterpolation = false;
+
+ if ($this->literal("[", false)) {
+ $attrParts = array("[");
+ // keyword, string, operator
+ while (true) {
+ if ($this->literal("]", false)) {
+ $this->count--;
+ break; // get out early
+ }
+
+ if ($this->match('\s+', $m)) {
+ $attrParts[] = " ";
+ continue;
+ }
+ if ($this->string($str)) {
+ // escape parent selector, (yuck)
+ foreach ($str[2] as &$chunk) {
+ $chunk = str_replace($this->lessc->parentSelector, "$&$", $chunk);
+ }
+
+ $attrParts[] = $str;
+ $hasInterpolation = true;
+ continue;
+ }
+
+ if ($this->keyword($word)) {
+ $attrParts[] = $word;
+ continue;
+ }
+
+ if ($this->interpolation($inter, false)) {
+ $attrParts[] = $inter;
+ $hasInterpolation = true;
+ continue;
+ }
+
+ // operator, handles attr namespace too
+ if ($this->match('[|-~\$\*\^=]+', $m)) {
+ $attrParts[] = $m[0];
+ continue;
+ }
+
+ break;
+ }
+
+ if ($this->literal("]", false)) {
+ $attrParts[] = "]";
+ foreach ($attrParts as $part) {
+ $parts[] = $part;
+ }
+ $hasExpression = $hasExpression || $hasInterpolation;
+ return true;
+ }
+ $this->seek($s);
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ // a space separated list of selectors
+ protected function tag(&$tag, $simple = false) {
+ if ($simple) {
+ $chars = '^@,:;{}\][>\(\) "\'';
+ } else {
+ $chars = '^@,;{}["\'';
+ }
+ $s = $this->seek();
+
+ $hasExpression = false;
+ $parts = array();
+ while ($this->tagBracket($parts, $hasExpression));
+
+ $oldWhite = $this->eatWhiteDefault;
+ $this->eatWhiteDefault = false;
+
+ while (true) {
+ if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) {
+ $parts[] = $m[1];
+ if ($simple) break;
+
+ while ($this->tagBracket($parts, $hasExpression));
+ continue;
+ }
+
+ if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") {
+ if ($this->interpolation($interp)) {
+ $hasExpression = true;
+ $interp[2] = true; // don't unescape
+ $parts[] = $interp;
+ continue;
+ }
+
+ if ($this->literal("@")) {
+ $parts[] = "@";
+ continue;
+ }
+ }
+
+ if ($this->unit($unit)) { // for keyframes
+ $parts[] = $unit[1];
+ $parts[] = $unit[2];
+ continue;
+ }
+
+ break;
+ }
+
+ $this->eatWhiteDefault = $oldWhite;
+ if (!$parts) {
+ $this->seek($s);
+ return false;
+ }
+
+ if ($hasExpression) {
+ $tag = array("exp", array("string", "", $parts));
+ } else {
+ $tag = trim(implode($parts));
+ }
+
+ $this->whitespace();
+ return true;
+ }
+
+ // a css function
+ protected function func(&$func) {
+ $s = $this->seek();
+
+ if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) {
+ $fname = $m[1];
+
+ $sPreArgs = $this->seek();
+
+ $args = array();
+ while (true) {
+ $ss = $this->seek();
+ // this ugly nonsense is for ie filter properties
+ if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
+ $args[] = array("string", "", array($name, "=", $value));
+ } else {
+ $this->seek($ss);
+ if ($this->expressionList($value)) {
+ $args[] = $value;
+ }
+ }
+
+ if (!$this->literal(',')) break;
+ }
+ $args = array('list', ',', $args);
+
+ if ($this->literal(')')) {
+ $func = array('function', $fname, $args);
+ return true;
+ } elseif ($fname == 'url') {
+ // couldn't parse and in url? treat as string
+ $this->seek($sPreArgs);
+ if ($this->openString(")", $string) && $this->literal(")")) {
+ $func = array('function', $fname, $string);
+ return true;
+ }
+ }
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ // consume a less variable
+ protected function variable(&$name) {
+ $s = $this->seek();
+ if ($this->literal($this->lessc->vPrefix, false) &&
+ ($this->variable($sub) || $this->keyword($name))
+ ) {
+ if (!empty($sub)) {
+ $name = array('variable', $sub);
+ } else {
+ $name = $this->lessc->vPrefix.$name;
+ }
+ return true;
+ }
+
+ $name = null;
+ $this->seek($s);
+ return false;
+ }
+
+ /**
+ * Consume an assignment operator
+ * Can optionally take a name that will be set to the current property name
+ */
+ protected function assign($name = null) {
+ if ($name) $this->currentProperty = $name;
+ return $this->literal(':') || $this->literal('=');
+ }
+
+ // consume a keyword
+ protected function keyword(&$word) {
+ if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
+ $word = $m[1];
+ return true;
+ }
+ return false;
+ }
+
+ // consume an end of statement delimiter
+ protected function end() {
+ if ($this->literal(';', false)) {
+ return true;
+ } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
+ // if there is end of file or a closing block next then we don't need a ;
+ return true;
+ }
+ return false;
+ }
+
+ protected function guards(&$guards) {
+ $s = $this->seek();
+
+ if (!$this->literal("when")) {
+ $this->seek($s);
+ return false;
+ }
+
+ $guards = array();
+
+ while ($this->guardGroup($g)) {
+ $guards[] = $g;
+ if (!$this->literal(",")) break;
+ }
+
+ if (count($guards) == 0) {
+ $guards = null;
+ $this->seek($s);
+ return false;
+ }
+
+ return true;
+ }
+
+ // a bunch of guards that are and'd together
+ // TODO rename to guardGroup
+ protected function guardGroup(&$guardGroup) {
+ $s = $this->seek();
+ $guardGroup = array();
+ while ($this->guard($guard)) {
+ $guardGroup[] = $guard;
+ if (!$this->literal("and")) break;
+ }
+
+ if (count($guardGroup) == 0) {
+ $guardGroup = null;
+ $this->seek($s);
+ return false;
+ }
+
+ return true;
+ }
+
+ protected function guard(&$guard) {
+ $s = $this->seek();
+ $negate = $this->literal("not");
+
+ if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) {
+ $guard = $exp;
+ if ($negate) $guard = array("negate", $guard);
+ return true;
+ }
+
+ $this->seek($s);
+ return false;
+ }
+
+ /* raw parsing functions */
+
+ protected function literal($what, $eatWhitespace = null) {
+ if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
+
+ // shortcut on single letter
+ if (!isset($what[1]) && isset($this->buffer[$this->count])) {
+ if ($this->buffer[$this->count] == $what) {
+ if (!$eatWhitespace) {
+ $this->count++;
+ return true;
+ }
+ // goes below...
+ } else {
+ return false;
+ }
+ }
+
+ if (!isset(self::$literalCache[$what])) {
+ self::$literalCache[$what] = lessc::preg_quote($what);
+ }
+
+ return $this->match(self::$literalCache[$what], $m, $eatWhitespace);
+ }
+
+ protected function genericList(&$out, $parseItem, $delim="", $flatten=true) {
+ $s = $this->seek();
+ $items = array();
+ while ($this->$parseItem($value)) {
+ $items[] = $value;
+ if ($delim) {
+ if (!$this->literal($delim)) break;
+ }
+ }
+
+ if (count($items) == 0) {
+ $this->seek($s);
+ return false;
+ }
+
+ if ($flatten && count($items) == 1) {
+ $out = $items[0];
+ } else {
+ $out = array("list", $delim, $items);
+ }
+
+ return true;
+ }
+
+
+ // advance counter to next occurrence of $what
+ // $until - don't include $what in advance
+ // $allowNewline, if string, will be used as valid char set
+ protected function to($what, &$out, $until = false, $allowNewline = false) {
+ if (is_string($allowNewline)) {
+ $validChars = $allowNewline;
+ } else {
+ $validChars = $allowNewline ? "." : "[^\n]";
+ }
+ if (!$this->match('('.$validChars.'*?)'.lessc::preg_quote($what), $m, !$until)) return false;
+ if ($until) $this->count -= strlen($what); // give back $what
+ $out = $m[1];
+ return true;
+ }
+
+ // try to match something on head of buffer
+ protected function match($regex, &$out, $eatWhitespace = null) {
+ if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
+
+ $r = '/'.$regex.($eatWhitespace && !$this->writeComments ? '\s*' : '').'/Ais';
+ if (preg_match($r, $this->buffer, $out, null, $this->count)) {
+ $this->count += strlen($out[0]);
+ if ($eatWhitespace && $this->writeComments) $this->whitespace();
+ return true;
+ }
+ return false;
+ }
+
+ // match some whitespace
+ protected function whitespace() {
+ if ($this->writeComments) {
+ $gotWhite = false;
+ while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
+ if (isset($m[1]) && empty($this->seenComments[$this->count])) {
+ $this->append(array("comment", $m[1]));
+ $this->seenComments[$this->count] = true;
+ }
+ $this->count += strlen($m[0]);
+ $gotWhite = true;
+ }
+ return $gotWhite;
+ } else {
+ $this->match("", $m);
+ return strlen($m[0]) > 0;
+ }
+ }
+
+ // match something without consuming it
+ protected function peek($regex, &$out = null, $from=null) {
+ if (is_null($from)) $from = $this->count;
+ $r = '/'.$regex.'/Ais';
+ $result = preg_match($r, $this->buffer, $out, null, $from);
+
+ return $result;
+ }
+
+ // seek to a spot in the buffer or return where we are on no argument
+ protected function seek($where = null) {
+ if ($where === null) return $this->count;
+ else $this->count = $where;
+ return true;
+ }
+
+ /* misc functions */
+
+ public function throwError($msg = "parse error", $count = null) {
+ $count = is_null($count) ? $this->count : $count;
+
+ $line = $this->line +
+ substr_count(substr($this->buffer, 0, $count), "\n");
+
+ if (!empty($this->sourceName)) {
+ $loc = "$this->sourceName on line $line";
+ } else {
+ $loc = "line: $line";
+ }
+
+ // TODO this depends on $this->count
+ if ($this->peek("(.*?)(\n|$)", $m, $count)) {
+ throw new exception("$msg: failed at `$m[1]` $loc");
+ } else {
+ throw new exception("$msg: $loc");
+ }
+ }
+
+ protected function pushBlock($selectors=null, $type=null) {
+ $b = new stdclass;
+ $b->parent = $this->env;
+
+ $b->type = $type;
+ $b->id = self::$nextBlockId++;
+
+ $b->isVararg = false; // TODO: kill me from here
+ $b->tags = $selectors;
+
+ $b->props = array();
+ $b->children = array();
+
+ $this->env = $b;
+ return $b;
+ }
+
+ // push a block that doesn't multiply tags
+ protected function pushSpecialBlock($type) {
+ return $this->pushBlock(null, $type);
+ }
+
+ // append a property to the current block
+ protected function append($prop, $pos = null) {
+ if ($pos !== null) $prop[-1] = $pos;
+ $this->env->props[] = $prop;
+ }
+
+ // pop something off the stack
+ protected function pop() {
+ $old = $this->env;
+ $this->env = $this->env->parent;
+ return $old;
+ }
+
+ // remove comments from $text
+ // todo: make it work for all functions, not just url
+ protected function removeComments($text) {
+ $look = array(
+ 'url(', '//', '/*', '"', "'"
+ );
+
+ $out = '';
+ $min = null;
+ while (true) {
+ // find the next item
+ foreach ($look as $token) {
+ $pos = strpos($text, $token);
+ if ($pos !== false) {
+ if (!isset($min) || $pos < $min[1]) $min = array($token, $pos);
+ }
+ }
+
+ if (is_null($min)) break;
+
+ $count = $min[1];
+ $skip = 0;
+ $newlines = 0;
+ switch ($min[0]) {
+ case 'url(':
+ if (preg_match('/url\(.*?\)/', $text, $m, 0, $count))
+ $count += strlen($m[0]) - strlen($min[0]);
+ break;
+ case '"':
+ case "'":
+ if (preg_match('/'.$min[0].'.*?(?indentLevel = 0;
+ }
+
+ public function indentStr($n = 0) {
+ return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
+ }
+
+ public function property($name, $value) {
+ return $name . $this->assignSeparator . $value . ";";
+ }
+
+ protected function isEmpty($block) {
+ if (empty($block->lines)) {
+ foreach ($block->children as $child) {
+ if (!$this->isEmpty($child)) return false;
+ }
+
+ return true;
+ }
+ return false;
+ }
+
+ public function block($block) {
+ if ($this->isEmpty($block)) return;
+
+ $inner = $pre = $this->indentStr();
+
+ $isSingle = !$this->disableSingle &&
+ is_null($block->type) && count($block->lines) == 1;
+
+ if (!empty($block->selectors)) {
+ $this->indentLevel++;
+
+ if ($this->breakSelectors) {
+ $selectorSeparator = $this->selectorSeparator . $this->break . $pre;
+ } else {
+ $selectorSeparator = $this->selectorSeparator;
+ }
+
+ echo $pre .
+ implode($selectorSeparator, $block->selectors);
+ if ($isSingle) {
+ echo $this->openSingle;
+ $inner = "";
+ } else {
+ echo $this->open . $this->break;
+ $inner = $this->indentStr();
+ }
+
+ }
+
+ if (!empty($block->lines)) {
+ $glue = $this->break.$inner;
+ echo $inner . implode($glue, $block->lines);
+ if (!$isSingle && !empty($block->children)) {
+ echo $this->break;
+ }
+ }
+
+ foreach ($block->children as $child) {
+ $this->block($child);
+ }
+
+ if (!empty($block->selectors)) {
+ if (!$isSingle && empty($block->children)) echo $this->break;
+
+ if ($isSingle) {
+ echo $this->closeSingle . $this->break;
+ } else {
+ echo $pre . $this->close . $this->break;
+ }
+
+ $this->indentLevel--;
+ }
+ }
+}
+
+/**
+ * Class for compressed result
+ */
+class lessc_formatter_compressed extends lessc_formatter_classic {
+ public $disableSingle = true;
+ public $open = "{";
+ public $selectorSeparator = ",";
+ public $assignSeparator = ":";
+ public $break = "";
+ public $compressColors = true;
+
+ public function indentStr($n = 0) {
+ return "";
+ }
+}
+
+/**
+ * Class for lessjs
+ */
+class lessc_formatter_lessjs extends lessc_formatter_classic {
+ public $disableSingle = true;
+ public $breakSelectors = true;
+ public $assignSeparator = ": ";
+ public $selectorSeparator = ",";
+}
\ No newline at end of file
diff --git a/htdocs/core/class/stats.class.php b/htdocs/core/class/stats.class.php
index 08025161a0d..8df81bd4495 100644
--- a/htdocs/core/class/stats.class.php
+++ b/htdocs/core/class/stats.class.php
@@ -39,10 +39,12 @@ abstract class Stats
* @param int $endyear Start year
* @param int $startyear End year
* @param int $cachedelay Delay we accept for cache file (0=No read, no save of cache, -1=No read but save)
- * @param int $format 0=Label of absiss is a translated text, 1=Label of absiss is month number, 2=Label of absiss is first letter of month
- * @return array Array of values
+ * @param int $format 0=Label of absiss is a translated text, 1=Label of absiss is month number, 2=Label of absiss is first letter of month
+ * @param int $startmonth month of the fiscal year start min 1 max 12 ; if 1 = january
+ * @return array Array of values
+
*/
- public function getNbByMonthWithPrevYear($endyear, $startyear, $cachedelay = 0, $format = 0)
+ public function getNbByMonthWithPrevYear($endyear, $startyear, $cachedelay = 0, $format = 0, $startmonth = 1)
{
global $conf,$user,$langs;
@@ -86,6 +88,8 @@ abstract class Stats
else
{
$year=$startyear;
+ $sm = $startmonth - 1;
+ if ($sm != 0) $year = $year - 1;
while ($year <= $endyear)
{
$datay[$year] = $this->getNbByMonth($year, $format);
@@ -96,11 +100,11 @@ abstract class Stats
for ($i = 0 ; $i < 12 ; $i++)
{
- $data[$i][]=$datay[$endyear][$i][0];
+ $data[$i][]=$datay[$endyear][($i+$sm)%12][0];
$year=$startyear;
while($year <= $endyear)
{
- $data[$i][]=$datay[$year][$i][1];
+ $data[$i][]=$datay[$year - (1 - ((int) ($i+$sm)/12)) + ($sm == 0 ? 1 : 0)][($i+$sm)%12][1];
$year++;
}
}
@@ -134,9 +138,10 @@ abstract class Stats
* @param int $startyear End year
* @param int $cachedelay Delay we accept for cache file (0=No read, no save of cache, -1=No read but save)
* @param int $format 0=Label of absiss is a translated text, 1=Label of absiss is month number, 2=Label of absiss is first letter of month
+ * @param int $startmonth month of the fiscal year start min 1 max 12 ; if 1 = january
* @return array Array of values
*/
- public function getAmountByMonthWithPrevYear($endyear, $startyear, $cachedelay = 0, $format = 0)
+ public function getAmountByMonthWithPrevYear($endyear, $startyear, $cachedelay = 0, $format = 0, $startmonth = 1)
{
global $conf,$user,$langs;
@@ -181,6 +186,8 @@ abstract class Stats
else
{
$year=$startyear;
+ $sm = $startmonth - 1;
+ if ($sm != 0) $year = $year - 1;
while($year <= $endyear)
{
$datay[$year] = $this->getAmountByMonth($year, $format);
@@ -191,11 +198,11 @@ abstract class Stats
// $data = array('xval'=>array(0=>xlabel,1=>yval1,2=>yval2...),...)
for ($i = 0 ; $i < 12 ; $i++)
{
- $data[$i][]=$datay[$endyear][$i][0]; // set label
+ $data[$i][]=$datay[$endyear][($i+$sm)%12][0]; // set label
$year=$startyear;
while($year <= $endyear)
{
- $data[$i][]=$datay[$year][$i][1]; // set yval for x=i
+ $data[$i][]=$datay[$year - (1 - ((int) ($i+$sm)/12)) + ($sm == 0 ? 1 : 0)][($i+$sm)%12][1]; // set yval for x=i
$year++;
}
}
@@ -411,7 +418,8 @@ abstract class Stats
// phpcs:disable PEAR.NamingConventions.ValidFunctionName.PublicUnderscore
/**
- * Renvoie le nombre de proposition par mois pour une annee donnee
+ * Renvoie le nombre de documents par mois pour une annee donnee
+ * Return number of documents per month for a given year
*
* @param int $year Year
* @param string $sql SQL
@@ -470,7 +478,8 @@ abstract class Stats
// phpcs:disable PEAR.NamingConventions.ValidFunctionName.PublicUnderscore
/**
- * Renvoie le nombre d'element par mois pour une annee donnee
+ * Renvoie le montant totalise par mois pour une annee donnee
+ * Return the amount per month for a given year
*
* @param int $year Year
* @param string $sql SQL
@@ -527,6 +536,7 @@ abstract class Stats
// phpcs:disable PEAR.NamingConventions.ValidFunctionName.PublicUnderscore
/**
* Renvoie le montant moyen par mois pour une annee donnee
+ * Return the amount average par month for a given year
*
* @param int $year Year
* @param string $sql SQL
diff --git a/htdocs/core/class/utils.class.php b/htdocs/core/class/utils.class.php
index f86d6570e5c..be220c90487 100644
--- a/htdocs/core/class/utils.class.php
+++ b/htdocs/core/class/utils.class.php
@@ -802,7 +802,7 @@ class Utils
dol_include_once('/core/lib/files.lib.php');
- $nbSaves = ! empty($conf->global->SYSLOG_FILE_SAVES) ? intval($conf->global->SYSLOG_FILE_SAVES) : 14;
+ $nbSaves = empty($conf->global->SYSLOG_FILE_SAVES) ? 10 : intval($conf->global->SYSLOG_FILE_SAVES);
if (empty($conf->global->SYSLOG_FILE)) {
$mainlogdir = DOL_DATA_ROOT;
diff --git a/htdocs/core/js/lib_head.js.php b/htdocs/core/js/lib_head.js.php
index cf8557de961..d177e7d99c1 100644
--- a/htdocs/core/js/lib_head.js.php
+++ b/htdocs/core/js/lib_head.js.php
@@ -856,7 +856,8 @@ function newpopup(url, title) {
}
/**
- * Function show document preview. Use the "dialog" function.
+ * Function show document preview. It uses the "dialog" function.
+ * The a tag around the img must have the src='', class='documentpreview', mime='image/xxx', target='_blank' from getAdvancedPreviewUrl().
*
* @param string file Url
* @param string type Mime file type ("image/jpeg", "application/pdf", "text/html")
diff --git a/htdocs/core/lib/files.lib.php b/htdocs/core/lib/files.lib.php
index e126ff2b7f0..a852c78d08b 100644
--- a/htdocs/core/lib/files.lib.php
+++ b/htdocs/core/lib/files.lib.php
@@ -555,6 +555,7 @@ function dol_count_nb_of_line($file)
*
* @param string $pathoffile Path of file
* @return integer File size
+ * @see dol_print_size()
*/
function dol_filesize($pathoffile)
{
@@ -985,6 +986,7 @@ function dolCheckVirus($src_file)
* - This function can be used only into a HTML page context. Use dol_move if you are outside.
* - Test on antivirus is always done (if antivirus set).
* - Database of files is NOT updated (this is done by dol_add_file_process() that calls this function).
+ * - Extension .noexe may be added if file is executable and MAIN_DOCUMENT_IS_OUTSIDE_WEBROOT_SO_NOEXE_NOT_REQUIRED is not set.
*
* @param string $src_file Source full path filename ($_FILES['field']['tmp_name'])
* @param string $dest_file Target full path filename ($_FILES['field']['name'])
@@ -1063,15 +1065,15 @@ function dol_move_uploaded_file($src_file, $dest_file, $allowoverwrite, $disable
// Security:
// We refuse cache files/dirs, upload using .. and pipes into filenames.
- if (preg_match('/^\./', $src_file) || preg_match('/\.\./', $src_file) || preg_match('/[<>|]/', $src_file))
+ if (preg_match('/^\./', basename($src_file)) || preg_match('/\.\./', $src_file) || preg_match('/[<>|]/', $src_file))
{
dol_syslog("Refused to deliver file ".$src_file, LOG_WARNING);
return -1;
}
// Security:
- // On interdit fichiers caches, remontees de repertoire ainsi que les pipe dans les noms de fichiers.
- if (preg_match('/^\./', $dest_file) || preg_match('/\.\./', $dest_file) || preg_match('/[<>|]/', $dest_file))
+ // We refuse cache files/dirs, upload using .. and pipes into filenames.
+ if (preg_match('/^\./', basename($dest_file)) || preg_match('/\.\./', $dest_file) || preg_match('/[<>|]/', $dest_file))
{
dol_syslog("Refused to deliver file ".$dest_file, LOG_WARNING);
return -2;
@@ -1888,24 +1890,87 @@ function dol_convert_file($fileinput, $ext = 'png', $fileoutput = '', $page = ''
*/
function dol_compress_file($inputfile, $outputfile, $mode = "gz")
{
+ global $conf;
+
$foundhandler=0;
try
{
+ dol_syslog("dol_compress_file mode=".$mode." inputfile=".$inputfile." outputfile=".$outputfile);
+
$data = implode("", file(dol_osencode($inputfile)));
if ($mode == 'gz') { $foundhandler=1; $compressdata = gzencode($data, 9); }
elseif ($mode == 'bz') { $foundhandler=1; $compressdata = bzcompress($data, 9); }
elseif ($mode == 'zip')
{
+ if (class_exists('ZipArchive') && ! empty($conf->global->MAIN_USE_ZIPARCHIVE_FOR_ZIP_COMPRESS))
+ {
+ $foundhandler=1;
+
+ $rootPath = realpath($inputfile);
+
+ dol_syslog("Class ZipArchive is set so we zip using ZipArchive to zip into ".$outputfile.' rootPath='.$rootPath);
+ $zip = new ZipArchive;
+
+ if ($zip->open($outputfile, ZipArchive::CREATE) !== true) {
+ $errormsg="Failed to open file ".$outputfile."\n";
+ dol_syslog("dol_compress_file failure - ".$errormsg, LOG_ERR);
+ return -6;
+ }
+
+ // Create recursive directory iterator
+ /** @var SplFileInfo[] $files */
+ $files = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($rootPath),
+ RecursiveIteratorIterator::LEAVES_ONLY
+ );
+
+ foreach ($files as $name => $file)
+ {
+ // Skip directories (they would be added automatically)
+ if (!$file->isDir())
+ {
+ // Get real and relative path for current file
+ $filePath = $file->getRealPath();
+ $relativePath = substr($filePath, strlen($rootPath) + 1);
+
+ // Add current file to archive
+ $zip->addFile($filePath, $relativePath);
+ }
+ }
+
+ // Zip archive will be created only after closing object
+ $zip->close();
+
+ dol_syslog("dol_compress_file success - ".count($zip->numFiles)." files");
+ return 1;
+ }
+
if (defined('ODTPHP_PATHTOPCLZIP'))
{
$foundhandler=1;
include_once ODTPHP_PATHTOPCLZIP.'/pclzip.lib.php';
$archive = new PclZip($outputfile);
- $archive->add($inputfile, PCLZIP_OPT_REMOVE_PATH, dirname($inputfile));
- //$archive->add($inputfile);
- return 1;
+ $result = $archive->add($inputfile, PCLZIP_OPT_REMOVE_PATH, dirname($inputfile));
+
+ if ($result === 0)
+ {
+ global $errormsg;
+ $errormsg=$archive->errorInfo(true);
+ dol_syslog("dol_compress_file failure - ".$errormsg, LOG_ERR);
+ if ($archive->errorCode() == PCLZIP_ERR_WRITE_OPEN_FAIL)
+ {
+ dol_syslog("dol_compress_file error PCLZIP_ERR_WRITE_OPEN_FAIL", LOG_ERR);
+ return -4;
+ }
+ return -3;
+ }
+ else
+ {
+ dol_syslog("dol_compress_file success - ".count($result)." files");
+ return 1;
+ }
}
}
@@ -1941,9 +2006,9 @@ function dol_compress_file($inputfile, $outputfile, $mode = "gz")
*/
function dol_uncompress($inputfile, $outputdir)
{
- global $langs;
+ global $conf, $langs;
- if (defined('ODTPHP_PATHTOPCLZIP'))
+ if (defined('ODTPHP_PATHTOPCLZIP') && empty($conf->global->MAIN_USE_ZIPARCHIVE_FOR_ZIP_UNCOMPRESS))
{
dol_syslog("Constant ODTPHP_PATHTOPCLZIP for pclzip library is set to ".ODTPHP_PATHTOPCLZIP.", so we use Pclzip to unzip into ".$outputdir);
include_once ODTPHP_PATHTOPCLZIP.'/pclzip.lib.php';
@@ -2164,6 +2229,12 @@ function dol_check_secure_access_document($modulepart, $original_file, $entity,
$accessallowed=($user->admin && basename($original_file) == $original_file && preg_match('/^dolibarr.*\.log$/', basename($original_file)));
$original_file=$dolibarr_main_data_root.'/'.$original_file;
}
+ // Wrapping for *.log files, like when used with url http://.../document.php?modulepart=logs&file=dolibarr.log
+ elseif ($modulepart == 'doctemplateswebsite' && !empty($dolibarr_main_data_root))
+ {
+ $accessallowed=($fuser->rights->website->write && preg_match('/\.jpg$/i', basename($original_file)));
+ $original_file=$dolibarr_main_data_root.'/doctemplates/websites/'.$original_file;
+ }
// Wrapping for *.zip files, like when used with url http://.../document.php?modulepart=packages&file=module_myfile.zip
elseif ($modulepart == 'packages' && !empty($dolibarr_main_data_root))
{
@@ -2193,10 +2264,10 @@ function dol_check_secure_access_document($modulepart, $original_file, $entity,
$original_file=$conf->adherent->dir_output.'/'.$original_file;
}
// Wrapping pour les apercu factures
- elseif ($modulepart == 'apercufacture' && !empty($conf->facture->dir_output))
+ elseif ($modulepart == 'apercufacture' && !empty($conf->facture->multidir_output[$entity]))
{
if ($fuser->rights->facture->{$lire}) $accessallowed=1;
- $original_file=$conf->facture->dir_output.'/'.$original_file;
+ $original_file=$conf->facture->multidir_output[$entity].'/'.$original_file;
}
// Wrapping pour les apercu propal
elseif ($modulepart == 'apercupropal' && !empty($conf->propal->multidir_output[$entity]))
@@ -2205,10 +2276,10 @@ function dol_check_secure_access_document($modulepart, $original_file, $entity,
$original_file=$conf->propal->multidir_output[$entity].'/'.$original_file;
}
// Wrapping pour les apercu commande
- elseif ($modulepart == 'apercucommande' && !empty($conf->commande->dir_output))
+ elseif ($modulepart == 'apercucommande' && !empty($conf->commande->multidir_output[$entity]))
{
if ($fuser->rights->commande->{$lire}) $accessallowed=1;
- $original_file=$conf->commande->dir_output.'/'.$original_file;
+ $original_file=$conf->commande->multidir_output[$entity].'/'.$original_file;
}
// Wrapping pour les apercu intervention
elseif (($modulepart == 'apercufichinter' || $modulepart == 'apercuficheinter') && !empty($conf->ficheinter->dir_output))
@@ -2404,13 +2475,13 @@ function dol_check_secure_access_document($modulepart, $original_file, $entity,
}
// Wrapping for invoices
- elseif (($modulepart == 'facture' || $modulepart == 'invoice') && !empty($conf->facture->dir_output))
+ elseif (($modulepart == 'facture' || $modulepart == 'invoice') && !empty($conf->facture->multidir_output[$entity]))
{
if ($fuser->rights->facture->{$lire} || preg_match('/^specimen/i', $original_file))
{
$accessallowed=1;
}
- $original_file=$conf->facture->dir_output.'/'.$original_file;
+ $original_file=$conf->facture->multidir_output[$entity].'/'.$original_file;
$sqlprotectagainstexternals = "SELECT fk_soc as fk_soc FROM ".MAIN_DB_PREFIX."facture WHERE ref='".$db->escape($refname)."' AND entity=".$conf->entity;
}
// Wrapping for mass actions
@@ -2428,15 +2499,23 @@ function dol_check_secure_access_document($modulepart, $original_file, $entity,
{
$accessallowed=1;
}
- $original_file=$conf->commande->dir_output.'/temp/massgeneration/'.$user->id.'/'.$original_file;
+ $original_file=$conf->commande->multidir_output[$entity].'/temp/massgeneration/'.$user->id.'/'.$original_file;
}
+ elseif ($modulepart == 'massfilesarea_sendings')
+ {
+ if ($fuser->rights->expedition->{$lire} || preg_match('/^specimen/i', $original_file))
+ {
+ $accessallowed=1;
+ }
+ $original_file=$conf->expedition->dir_output.'/sending/temp/massgeneration/'.$user->id.'/'.$original_file;
+ }
elseif ($modulepart == 'massfilesarea_invoices')
{
if ($fuser->rights->facture->{$lire} || preg_match('/^specimen/i', $original_file))
{
$accessallowed=1;
}
- $original_file=$conf->facture->dir_output.'/temp/massgeneration/'.$user->id.'/'.$original_file;
+ $original_file=$conf->facture->multidir_output[$entity].'/temp/massgeneration/'.$user->id.'/'.$original_file;
}
elseif ($modulepart == 'massfilesarea_expensereport')
{
@@ -2520,13 +2599,13 @@ function dol_check_secure_access_document($modulepart, $original_file, $entity,
}
// Wrapping pour les commandes
- elseif (($modulepart == 'commande' || $modulepart == 'order') && !empty($conf->commande->dir_output))
+ elseif (($modulepart == 'commande' || $modulepart == 'order') && !empty($conf->commande->multidir_output[$entity]))
{
if ($fuser->rights->commande->{$lire} || preg_match('/^specimen/i', $original_file))
{
$accessallowed=1;
}
- $original_file=$conf->commande->dir_output.'/'.$original_file;
+ $original_file=$conf->commande->multidir_output[$entity].'/'.$original_file;
$sqlprotectagainstexternals = "SELECT fk_soc as fk_soc FROM ".MAIN_DB_PREFIX."commande WHERE ref='".$db->escape($refname)."' AND entity=".$conf->entity;
}
@@ -2849,7 +2928,7 @@ function dol_check_secure_access_document($modulepart, $original_file, $entity,
{
if (empty($conf->$modulepart->dir_output)) // modulepart not supported
{
- dol_print_error('', 'Error call dol_check_secure_access_document with not supported value for modulepart parameter ('.$modulepart.')');
+ dol_print_error('', 'Error call dol_check_secure_access_document with not supported value for modulepart parameter ('.$modulepart.'). The module for this modulepart value may not be activated.');
exit;
}
diff --git a/htdocs/core/lib/functions.lib.php b/htdocs/core/lib/functions.lib.php
index e34c3e3b7b5..e851509a000 100644
--- a/htdocs/core/lib/functions.lib.php
+++ b/htdocs/core/lib/functions.lib.php
@@ -5,7 +5,7 @@
* Copyright (C) 2004 Sebastien Di Cintio
* Copyright (C) 2004 Benoit Mortier
* Copyright (C) 2004 Christophe Combelles
- * Copyright (C) 2005-2017 Regis Houssin
+ * Copyright (C) 2005-2019 Regis Houssin
* Copyright (C) 2008 Raphael Bertrand (Resultic)
* Copyright (C) 2010-2018 Juanjo Menent
* Copyright (C) 2013 Cédric Salvador
@@ -75,7 +75,7 @@ function getDoliDBInstance($type, $host, $user, $pass, $name, $port)
* @param int $shared 0=Return id of current entity only,
* 1=Return id of current entity + shared entities (default)
* @param object $currentobject Current object if needed
- * @return mixed Entity id(s) to use
+ * @return mixed Entity id(s) to use ( eg. entity IN ('.getEntity(elementname).')' )
*/
function getEntity($element, $shared = 1, $currentobject = null)
{
@@ -95,6 +95,26 @@ function getEntity($element, $shared = 1, $currentobject = null)
}
}
+/**
+ * Set entity id to use when to create an object
+ *
+ * @param object $currentobject Current object
+ * @return mixed Entity id to use ( eg. entity = '.setEntity($object) )
+ */
+function setEntity($currentobject)
+{
+ global $conf, $mc;
+
+ if (is_object($mc))
+ {
+ return $mc->setEntity($currentobject);
+ }
+ else
+ {
+ return ((is_object($currentobject) && $currentobject->id > 0 && $currentobject->entity > 0) ? $currentobject->entity : $conf->entity);
+ }
+}
+
/**
* Return information about user browser
*
@@ -796,7 +816,8 @@ function dol_size($size, $type = '')
*/
function dol_sanitizeFileName($str, $newstr = '_', $unaccent = 1)
{
- $filesystem_forbidden_chars = array('<','>','/','\\','?','*','|','"','°');
+ // List of special chars for filenames are defined on page https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
+ $filesystem_forbidden_chars = array('<', '>', '/', '\\', '?', '*', '|', '"', ':', '°');
return dol_string_nospecial($unaccent?dol_string_unaccent($str):$str, $newstr, $filesystem_forbidden_chars);
}
diff --git a/htdocs/core/lib/functions2.lib.php b/htdocs/core/lib/functions2.lib.php
index 2f01cf441db..b4c8f6bf011 100644
--- a/htdocs/core/lib/functions2.lib.php
+++ b/htdocs/core/lib/functions2.lib.php
@@ -576,6 +576,7 @@ function isValidVATID($company)
{
$vatprefix = $company->country_code;
if ($vatprefix == 'GR') $vatprefix = '(EL|GR)';
+ elseif ($vatprefix == 'MC') $vatprefix = 'FR'; // Monaco is using french VAT numbers
else $vatprefix = preg_quote($vatprefix, '/');
if (! preg_match('/^'.$vatprefix.'[a-zA-Z0-9\-\.]{5,14}$/i', str_replace(' ', '', $company->tva_intra)))
{
diff --git a/htdocs/core/lib/order.lib.php b/htdocs/core/lib/order.lib.php
index 6e276dcf8f4..5727ececb5e 100644
--- a/htdocs/core/lib/order.lib.php
+++ b/htdocs/core/lib/order.lib.php
@@ -96,7 +96,7 @@ function commande_prepare_head(Commande $object)
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/class/link.class.php';
- $upload_dir = $conf->commande->dir_output . "/" . dol_sanitizeFileName($object->ref);
+ $upload_dir = $conf->commande->multidir_output[$object->entity] . "/" . dol_sanitizeFileName($object->ref);
$nbFiles = count(dol_dir_list($upload_dir, 'files', 0, '', '(\.meta|_preview.*\.png)$'));
$nbLinks=Link::count($db, $object->element, $object->id);
$head[$h][0] = DOL_URL_ROOT.'/commande/document.php?id='.$object->id;
diff --git a/htdocs/core/lib/usergroups.lib.php b/htdocs/core/lib/usergroups.lib.php
index 7da112fa255..7c97c38dd4f 100644
--- a/htdocs/core/lib/usergroups.lib.php
+++ b/htdocs/core/lib/usergroups.lib.php
@@ -295,10 +295,9 @@ function user_admin_prepare_head()
* @param boolean $foruserprofile Show for user profile view
* @return void
*/
-function show_theme($fuser, $edit = 0, $foruserprofile = false)
+function showSkins($fuser, $edit = 0, $foruserprofile = false)
{
global $conf,$langs,$db,$form;
- global $bc;
require_once DOL_DOCUMENT_ROOT . '/core/class/html.formother.class.php';
@@ -351,9 +350,7 @@ function show_theme($fuser, $edit = 0, $foruserprofile = false)
{
print ''.$langs->trans("DefaultSkin").' ';
print '';
- $url='https://www.dolistore.com/lang-en/4-skins';
- if (preg_match('/fr/i', $langs->defaultlang)) $url='https://www.dolistore.com/fr/4-themes';
- //if (preg_match('/es/i',$langs->defaultlang)) $url='http://www.dolistore.com/lang-es/4-themes';
+ $url='https://www.dolistore.com/4-skins';
print '';
print $langs->trans('DownloadMoreSkins');
print ' ';
@@ -432,7 +429,7 @@ function show_theme($fuser, $edit = 0, $foruserprofile = false)
print ' ';
print ''.$langs->trans("TopMenuDisableImages").' ';
print ''.($conf->global->THEME_TOPMENU_DISABLE_IMAGE?$conf->global->THEME_TOPMENU_DISABLE_IMAGE:$langs->trans("Default")).' ';
- print 'conf->THEME_ELDY_TEXTLINK)?" checked":"");
+ print ' conf->THEME_ELDY_TEXTLINK)?" checked":"");
print (empty($dolibarr_main_demo) && $edit)?'':' disabled="disabled"'; // Disabled for demo
print '> '.$langs->trans("UsePersonalValue").' ';
print '';
@@ -477,7 +474,7 @@ function show_theme($fuser, $edit = 0, $foruserprofile = false)
print ' ';
print ''.$langs->trans("TopMenuBackgroundColor").' ';
print ''.($conf->global->THEME_ELDY_TOPMENU_BACK1?$conf->global->THEME_ELDY_TOPMENU_BACK1:$langs->trans("Default")).' ';
- print 'conf->THEME_ELDY_TOPMENU_BACK1)?" checked":"");
+ print ' conf->THEME_ELDY_TOPMENU_BACK1)?" checked":"");
print (empty($dolibarr_main_demo) && $edit)?'':' disabled="disabled"'; // Disabled for demo
print '> '.$langs->trans("UsePersonalValue").' ';
print '';
@@ -524,7 +521,7 @@ function show_theme($fuser, $edit = 0, $foruserprofile = false)
print ' ';
print ''.$langs->trans("TopMenuBackgroundColor").' ';
print ''.($conf->global->THEME_ELDY_TOPMENU_BACK1?$conf->global->THEME_ELDY_TOPMENU_BACK1:$langs->trans("Default")).' ';
- print 'conf->THEME_ELDY_TOPMENU_BACK1)?" checked":"");
+ print ' conf->THEME_ELDY_TOPMENU_BACK1)?" checked":"");
print (empty($dolibarr_main_demo) && $edit)?'':' disabled="disabled"'; // Disabled for demo
print '> '.$langs->trans("UsePersonalValue").' ';
print '';
@@ -573,7 +570,7 @@ function show_theme($fuser, $edit = 0, $foruserprofile = false)
print ' ';
print ''.$langs->trans("TopMenuBackgroundColor").' ';
print ''.($conf->global->THEME_ELDY_TOPMENU_BACK1?$conf->global->THEME_ELDY_VERMENU_BACK1:$langs->trans("Default")).' ';
- print 'conf->THEME_ELDY_TOPMENU_BACK1)?" checked":"");
+ print ' conf->THEME_ELDY_TOPMENU_BACK1)?" checked":"");
print (empty($dolibarr_main_demo) && $edit)?'':' disabled="disabled"'; // Disabled for demo
print '> '.$langs->trans("UsePersonalValue").' ';
print '';
@@ -763,7 +760,7 @@ function show_theme($fuser, $edit = 0, $foruserprofile = false)
print ' ';
print ''.$langs->trans("TopMenuBackgroundColor").' ';
print ''.($conf->global->THEME_ELDY_TOPMENU_BACK1?$conf->global->THEME_ELDY_TEXTLINK:$langs->trans("Default")).' ';
- print ' conf->THEME_ELDY_TEXTLINK)?" checked":"");
+ print ' conf->THEME_ELDY_TEXTLINK)?" checked":"");
print (empty($dolibarr_main_demo) && $edit)?'':' disabled="disabled"'; // Disabled for demo
print '> '.$langs->trans("UsePersonalValue").' ';
print '';
@@ -813,9 +810,9 @@ function show_theme($fuser, $edit = 0, $foruserprofile = false)
/* Must first change option to choose color of highlight instead of yes or no.
print ' ';
print ''.$langs->trans("HighlightLinesOnMouseHover").' ';
- print ' global->THEME_ELDY_USE_HOVER?" checked":"").'> ';
- print ' '.$langs->trans("UsePersonalValue").' ';
- print ' ';
+ print ' global->THEME_ELDY_USE_HOVER?" checked":"").'> ';
+ print ' '.$langs->trans("UsePersonalValue").' ';
+ print ' ';
print ' ('.$langs->trans("NotSupportedByAllThemes").', '.$langs->trans("PressF5AfterChangingThis").')';
print ' ';
print ' ';
@@ -825,7 +822,7 @@ function show_theme($fuser, $edit = 0, $foruserprofile = false)
print '';
print ''.$langs->trans("HighlightLinesColor").' ';
print '';
- //print ' ';
+ //print ' ';
//print ' ('.$langs->trans("NotSupportedByAllThemes").', '.$langs->trans("PressF5AfterChangingThis").')';
if ($edit)
{
@@ -856,9 +853,9 @@ function show_theme($fuser, $edit = 0, $foruserprofile = false)
/* Must first change option to choose color of highlight instead of yes or no.
print ' ';
print ''.$langs->trans("HighlightLinesOnMouseHover").' ';
- print ' global->THEME_ELDY_USE_HOVER?" checked":"").'> ';
- print ' '.$langs->trans("UsePersonalValue").' ';
- print ' ';
+ print ' global->THEME_ELDY_USE_HOVER?" checked":"").'> ';
+ print ' '.$langs->trans("UsePersonalValue").' ';
+ print ' ';
print ' ('.$langs->trans("NotSupportedByAllThemes").', '.$langs->trans("PressF5AfterChangingThis").')';
print ' ';
print ' ';
@@ -869,7 +866,7 @@ function show_theme($fuser, $edit = 0, $foruserprofile = false)
print '';
print ''.$langs->trans("HighlightLinesChecked").' ';
print '';
- //print ' ';
+ //print ' ';
//print ' ('.$langs->trans("NotSupportedByAllThemes").', '.$langs->trans("PressF5AfterChangingThis").')';
if ($edit)
{
@@ -947,5 +944,45 @@ function show_theme($fuser, $edit = 0, $foruserprofile = false)
*/
}
+
+ // Use MAIN_OPTIMIZEFORTEXTBROWSER
+ if ($foruserprofile)
+ {
+ //$default=yn($conf->global->MAIN_OPTIMIZEFORCOLORBLIND);
+ $default=$langs->trans('No');
+ print ' ';
+ print ''.$langs->trans("MAIN_OPTIMIZEFORCOLORBLIND").' ';
+ print '';
+
+ $colorBlindOptions = array(
+ 0 => $langs->trans('No'),
+ 'flashy' => $langs->trans('Flashy'),
+ 'protanopia' => $langs->trans('Protanopia'),
+ 'deuteranopes' => $langs->trans('Deuteranopes'),
+ 'tritanopes' => $langs->trans('Tritanopes'),
+ );
+
+ if ($edit)
+ {
+ print $form->selectArray('MAIN_OPTIMIZEFORCOLORBLIND', $colorBlindOptions, $fuser->conf->MAIN_OPTIMIZEFORCOLORBLIND, 0);
+ }
+ else
+ {
+ if (!empty($fuser->conf->MAIN_OPTIMIZEFORCOLORBLIND) && isset($colorBlindOptions[$fuser->conf->MAIN_OPTIMIZEFORCOLORBLIND])){
+ print $colorBlindOptions[$fuser->conf->MAIN_OPTIMIZEFORCOLORBLIND];
+ }
+ else{
+ print yn(0);
+ }
+ }
+ print ' ('.$langs->trans("Default").': '.$default.' ) ';
+ print $form->textwithpicto('', $langs->trans("MAIN_OPTIMIZEFORCOLORBLINDDesc"));
+ print ' ';
+ print ' ';
+ }
+ else
+ {
+
+ }
print '';
}
diff --git a/htdocs/core/lib/website.lib.php b/htdocs/core/lib/website.lib.php
index 00716f6a3ab..74c06201507 100644
--- a/htdocs/core/lib/website.lib.php
+++ b/htdocs/core/lib/website.lib.php
@@ -22,78 +22,6 @@
*/
-/**
- * Convert a page content to have correct links (based on DOL_URL_ROOT) into an html content.
- * Used to ouput the page on the Preview from backoffice.
- *
- * @param Website $website Web site object
- * @param string $content Content to replace
- * @param int $removephppart 0=Replace PHP sections with a PHP badge. 1=Remove completely PHP sections.
- * @return boolean True if OK
- * @see dolWebsiteOutput() for function used to replace content in a web server context
- */
-function dolWebsiteReplacementOfLinks($website, $content, $removephppart = 0)
-{
- $nbrep = 0;
-
- // Replace php code. Note $content may come from database and does not contains body tags.
- $replacewith='...php...';
- if ($removephppart) $replacewith='';
- $content = preg_replace('/value="<\?php((?!\?>).)*\?>\n*/ims', 'value="'.$replacewith.'"', $content);
-
- $replacewith='"callto=#';
- if ($removephppart) $replacewith='';
- $content = preg_replace('/"callto:<\?php((?!\?>).)*\?>\n*/ims', $replacewith, $content);
-
- $replacewith='"mailto=#';
- if ($removephppart) $replacewith='';
- $content = preg_replace('/"mailto:<\?php((?!\?>).)*\?>\n*/ims', $replacewith, $content);
-
- $replacewith='src="php';
- if ($removephppart) $replacewith='';
- $content = preg_replace('/src="<\?php((?!\?>).)*\?>\n*/ims', $replacewith, $content);
-
- $replacewith='href="php';
- if ($removephppart) $replacewith='';
- $content = preg_replace('/href="<\?php((?!\?>).)*\?>\n*/ims', $replacewith, $content);
-
- //$replacewith='...php... ';
- $replacewith='...php... ';
- if ($removephppart) $replacewith='';
- //$content = preg_replace('/<\?php((?!\?toremove>).)*\?toremove>\n*/ims', $replacewith, $content);
- /*if ($content === null) {
- if (preg_last_error() == PREG_JIT_STACKLIMIT_ERROR) $content = 'preg_replace error (when removing php tags) PREG_JIT_STACKLIMIT_ERROR';
- }*/
- $content = dolStripPhpCode($content, $replacewith);
- //var_dump($content);
-
- // Replace relative link / with dolibarr URL
- $content = preg_replace('/(href=")\/\"/', '\1'.DOL_URL_ROOT.'/website/index.php?website='.$website->ref.'&pageid='.$website->fk_default_home.'"', $content, -1, $nbrep);
- // Replace relative link /xxx.php with dolibarr URL
- $content = preg_replace('/(href=")\/?([^:\"]*)(\.php\")/', '\1'.DOL_URL_ROOT.'/website/index.php?website='.$website->ref.'&pageref=\2"', $content, -1, $nbrep);
-
- // Fix relative link into medias with correct URL after the DOL_URL_ROOT: ../url("medias/
- $content = preg_replace('/url\((["\']?)medias\//', 'url(\1'.DOL_URL_ROOT.'/viewimage.php?modulepart=medias&file=', $content, -1, $nbrep);
- $content = preg_replace('/data-slide-bg=(["\']?)medias\//', 'data-slide-bg=\1'.DOL_URL_ROOT.'/viewimage.php?modulepart=medias&file=', $content, -1, $nbrep);
-
- // ]*src=")(medias\/)/', '\1'.DOL_URL_ROOT.'/viewimage.php?modulepart=medias&file=', $content, -1, $nbrep);
- // ]*src=")(?!(http|\/?viewimage|'.preg_quote(DOL_URL_ROOT, '/').'\/viewimage))/', '\1'.DOL_URL_ROOT.'/viewimage.php?modulepart=medias&file=', $content, -1, $nbrep);
- // ]*src=")(\/?viewimage\.php)/', '\1'.DOL_URL_ROOT.'/viewimage.php', $content, -1, $nbrep);
-
- // action="newpage.php" => action="dolibarr/website/index.php?website=...&pageref=newpage
- $content = preg_replace('/(action=")\/?([^:\"]*)(\.php\")/', '\1'.DOL_URL_ROOT.'/website/index.php?website='.$website->ref.'&pageref=\2"', $content, -1, $nbrep);
-
- // Fix relative link /document.php with correct URL after the DOL_URL_ROOT: ...href="/document.php?modulepart="
- $content=preg_replace('/(href=")(\/?document\.php\?[^\"]*modulepart=[^\"]*)(\")/', '\1'.DOL_URL_ROOT.'\2\3', $content, -1, $nbrep);
- $content=preg_replace('/(src=")(\/?document\.php\?[^\"]*modulepart=[^\"]*)(\")/', '\1'.DOL_URL_ROOT.'\2\3', $content, -1, $nbrep);
-
- return $content;
-}
-
-
/**
* Remove PHP code part from a string.
*
@@ -119,14 +47,15 @@ function dolStripPhpCode($str, $replacewith = '')
$newstr .= $part;
continue;
}
- //split on closing tag
+ // The second part is the php code. We split on closing tag
$partlings = explode('?>', $part);
if (!empty($partlings))
{
+ $phppart = $partlings[0];
//remove content before closing tag
- if (count($partlings) > 1) $partlings[0] = '';
+ if (count($partlings) > 1) $partlings[0] = ''; // Todo why a count > 1 and not >= 1 ?
//append to out string
- $newstr .= $replacewith.implode('', $partlings);
+ $newstr .= ''.$replacewith.' '.implode('', $partlings);
}
}
}
@@ -142,50 +71,154 @@ function dolStripPhpCode($str, $replacewith = '')
*/
function dolKeepOnlyPhpCode($str)
{
- $newstr = '';
+ $newstr = '';
- //split on each opening tag
- $parts = explode('', $part, 2);
- if (!empty($partlings))
- {
- $newstr .= $partlings[0].'?>';
- }
- else
- {
- $newstr .= $part.'?>';
- }
- }
- }
- return $newstr;
+ //split on each opening tag
+ $parts = explode('', $part, 2);
+ if (!empty($partlings))
+ {
+ $newstr .= $partlings[0].'?>';
+ }
+ else
+ {
+ $newstr .= $part.'?>';
+ }
+ }
+ }
+ return $newstr;
+}
+
+/**
+ * Convert a page content to have correct links (based on DOL_URL_ROOT) into an html content. It replaces also dynamic content with '...php...'
+ * Used to ouput the page on the Preview from backoffice.
+ *
+ * @param Website $website Web site object
+ * @param string $content Content to replace
+ * @param int $removephppart 0=Replace PHP sections with a PHP badge. 1=Remove completely PHP sections.
+ * @param string $contenttype Content type
+ * @param int $containerid Contenair id
+ * @return boolean True if OK
+ * @see dolWebsiteOutput() for function used to replace content in a web server context
+ */
+function dolWebsiteReplacementOfLinks($website, $content, $removephppart = 0, $contenttype = 'html', $containerid = '')
+{
+ $nbrep = 0;
+
+ dol_syslog('dolWebsiteReplacementOfLinks start (contenttype='.$contenttype." containerid=".$containerid." USEDOLIBARREDITOR=".(defined('USEDOLIBARREDITOR')?'1':'')." USEDOLIBARRSERVER=".(defined('USEDOLIBARRSERVER')?'1':'').')', LOG_DEBUG);
+ //if ($contenttype == 'html') { print $content;exit; }
+
+ // Replace php code. Note $content may come from database and does not contains body tags.
+ $replacewith='...php...';
+ if ($removephppart) $replacewith='';
+ $content = preg_replace('/value="<\?php((?!\?>).)*\?>\n*/ims', 'value="'.$replacewith.'"', $content);
+
+ $replacewith='"callto=#';
+ if ($removephppart) $replacewith='';
+ $content = preg_replace('/"callto:<\?php((?!\?>).)*\?>\n*/ims', $replacewith, $content);
+
+ $replacewith='"mailto=#';
+ if ($removephppart) $replacewith='';
+ $content = preg_replace('/"mailto:<\?php((?!\?>).)*\?>\n*/ims', $replacewith, $content);
+
+ $replacewith='src="php';
+ if ($removephppart) $replacewith='';
+ $content = preg_replace('/src="<\?php((?!\?>).)*\?>\n*/ims', $replacewith, $content);
+
+ $replacewith='href="php';
+ if ($removephppart) $replacewith='';
+ $content = preg_replace('/href="<\?php((?!\?>).)*\?>\n*/ims', $replacewith, $content);
+
+ //$replacewith='...php... ';
+ $replacewith='...php...';
+ if ($removephppart) $replacewith='';
+ //$content = preg_replace('/<\?php((?!\?toremove>).)*\?toremove>\n*/ims', $replacewith, $content);
+ /*if ($content === null) {
+ if (preg_last_error() == PREG_JIT_STACKLIMIT_ERROR) $content = 'preg_replace error (when removing php tags) PREG_JIT_STACKLIMIT_ERROR';
+ }*/
+ $content = dolStripPhpCode($content, $replacewith);
+ //var_dump($content);
+
+ // Protect the link styles.css.php to any replacement that we make after.
+ $content = str_replace('href="styles.css.php', 'href="!~!~!~styles.css.php', $content);
+ $content = str_replace('href="http', 'href="!~!~!~http', $content);
+ $content = str_replace('href="//', 'href="!~!~!~//', $content);
+ $content = str_replace('src="viewimage.php', 'src="!~!~!~/viewimage.php', $content);
+ $content = str_replace('src="/viewimage.php', 'src="!~!~!~/viewimage.php', $content);
+ $content = str_replace('src="'.DOL_URL_ROOT.'/viewimage.php', 'src="!~!~!~'.DOL_URL_ROOT.'/viewimage.php', $content);
+ $content = str_replace('href="document.php', 'href="!~!~!~/document.php', $content);
+ $content = str_replace('href="/document.php', 'href="!~!~!~/document.php', $content);
+ $content = str_replace('href="'.DOL_URL_ROOT.'/document.php', 'href="!~!~!~'.DOL_URL_ROOT.'/document.php', $content);
+
+ // Replace relative link '/' with dolibarr URL
+ $content = preg_replace('/(href=")\/\"/', '\1!~!~!~'.DOL_URL_ROOT.'/website/index.php?website='.$website->ref.'&pageid='.$website->fk_default_home.'"', $content, -1, $nbrep);
+ // Replace relative link /xxx.php#aaa or /xxx.php with dolibarr URL (we discard param ?...)
+ $content = preg_replace('/(href=")\/?([^:\"\!]*)\.php(#[^\"<>]*)?\"/', '\1!~!~!~'.DOL_URL_ROOT.'/website/index.php?website='.$website->ref.'&pageref=\2\3"', $content, -1, $nbrep);
+ // Replace relative link /xxx.php?a=b&c=d#aaa or /xxx.php?a=b&c=d with dolibarr URL
+ $content = preg_replace('/(href=")\/?([^:\"\!]*)\.php\?([^#\"<>]*)(#[^\"<>]*)?\"/', '\1!~!~!~'.DOL_URL_ROOT.'/website/index.php?website='.$website->ref.'&pageref=\2&\3\4"', $content, -1, $nbrep);
+
+ // Fix relative link into medias with correct URL after the DOL_URL_ROOT: ../url("medias/
+ $content = preg_replace('/url\((["\']?)medias\//', 'url(\1!~!~!~'.DOL_URL_ROOT.'/viewimage.php?modulepart=medias&file=', $content, -1, $nbrep);
+ $content = preg_replace('/data-slide-bg=(["\']?)medias\//', 'data-slide-bg=\1!~!~!~'.DOL_URL_ROOT.'/viewimage.php?modulepart=medias&file=', $content, -1, $nbrep);
+
+ // ]*src=")\/?medias\//', '\1!~!~!~'.DOL_URL_ROOT.'/viewimage.php?modulepart=medias&file=', $content, -1, $nbrep);
+ // ]*src=")\/?([^:\"\!]+)\"/', '\1!~!~!~'.DOL_URL_ROOT.'/viewimage.php?modulepart=medias&file=\2"', $content, -1, $nbrep);
+ // ]*src=")(\/?viewimage\.php)/', '\1!~!~!~'.DOL_URL_ROOT.'/viewimage.php', $content, -1, $nbrep);
+
+ // action="newpage.php" => action="dolibarr/website/index.php?website=...&pageref=newpage
+ $content = preg_replace('/(action=")\/?([^:\"]*)(\.php\")/', '\1!~!~!~'.DOL_URL_ROOT.'/website/index.php?website='.$website->ref.'&pageref=\2"', $content, -1, $nbrep);
+
+ // Fix relative link /document.php with correct URL after the DOL_URL_ROOT: ...href="/document.php?modulepart="
+ $content=preg_replace('/(href=")(\/?document\.php\?[^\"]*modulepart=[^\"]*)(\")/', '\1!~!~!~'.DOL_URL_ROOT.'\2\3', $content, -1, $nbrep);
+ $content=preg_replace('/(src=")(\/?document\.php\?[^\"]*modulepart=[^\"]*)(\")/', '\1!~!~!~'.DOL_URL_ROOT.'\2\3', $content, -1, $nbrep);
+
+ // Fix relative link /viewimage.php with correct URL after the DOL_URL_ROOT: ...href="/viewimage.php?modulepart="
+ $content=preg_replace('/(url\(")(\/?viewimage\.php\?[^\"]*modulepart=[^\"]*)(\")/', '\1!~!~!~'.DOL_URL_ROOT.'\2\3', $content, -1, $nbrep);
+
+ // Fix relative URL
+ $content = str_replace('src="!~!~!~/viewimage.php', 'src="!~!~!~'.DOL_URL_ROOT.'/viewimage.php', $content);
+ $content = str_replace('href="!~!~!~/document.php', 'href="!~!~!~'.DOL_URL_ROOT.'/document.php', $content);
+ // Remove the protection tag !~!~!~
+ $content = str_replace('!~!~!~', '', $content);
+
+ dol_syslog('dolWebsiteReplacementOfLinks end', LOG_DEBUG);
+ //if ($contenttype == 'html') { print $content;exit; }
+
+ return $content;
}
/**
* Render a string of an HTML content and output it.
* Used to ouput the page when viewed from server (Dolibarr or Apache).
*
- * @param string $content Content string
+ * @param string $content Content string
+ * @param string $contenttype Content type
+ * @param int $containerid Contenair id
* @return void
- * @see dolWebsiteReplacementOfLinks() for function used to replace content in the backoffice context when USEDOLIBARREDITOR is not on
+ * @see dolWebsiteReplacementOfLinks() for function used to replace content in the backoffice context.
*/
-function dolWebsiteOutput($content)
+function dolWebsiteOutput($content, $contenttype = 'html', $containerid = '')
{
global $db, $langs, $conf, $user;
global $dolibarr_main_url_root, $dolibarr_main_data_root;
- dol_syslog("dolWebsiteOutput start (USEDOLIBARRSERVER=".(defined('USEDOLIBARRSERVER')?'1':'')." USEDOLIBARREDITOR=".(defined('USEDOLIBARREDITOR')?'1':'').')');
+ dol_syslog("dolWebsiteOutput start (contenttype=".$contenttype." containerid=".$containerid." USEDOLIBARREDITOR=".(defined('USEDOLIBARREDITOR')?'1':'')." USEDOLIBARRSERVER=".(defined('USEDOLIBARRSERVER')?'1':'').')');
// Define $urlwithroot
$urlwithouturlroot=preg_replace('/'.preg_quote(DOL_URL_ROOT, '/').'$/i', '', trim($dolibarr_main_url_root));
@@ -195,43 +228,66 @@ function dolWebsiteOutput($content)
if (defined('USEDOLIBARREDITOR')) // REPLACEMENT OF LINKS When page called from Dolibarr editor
{
// We remove the part of content
- $content = preg_replace('/.*<\/head>/ims', '', $content);
- $content = preg_replace('/^.*]*)*>/ims', '', $content);
- $content = preg_replace('/<\/body(\s[^>]*)*>.*$/ims', '', $content);
+ if ($contenttype == 'html')
+ {
+ $content = preg_replace('/.*<\/head>/ims', '', $content);
+ $content = preg_replace('/^.*]*)*>/ims', '', $content);
+ $content = preg_replace('/<\/body(\s[^>]*)*>.*$/ims', '', $content);
+ }
}
elseif (defined('USEDOLIBARRSERVER')) // REPLACEMENT OF LINKS When page called from Dolibarr server
{
global $website;
+ // Protect the link styles.css.php to any replacement that we make after.
+ $content = str_replace('href="styles.css.php', 'href="!~!~!~styles.css.php', $content);
+ $content = str_replace('href="http', 'href="!~!~!~http', $content);
+ $content = str_replace('href="//', 'href="!~!~!~//', $content);
+ $content = str_replace('src="viewimage.php', 'src="!~!~!~/viewimage.php', $content);
+ $content = str_replace('src="/viewimage.php', 'src="!~!~!~/viewimage.php', $content);
+ $content = str_replace('src="'.DOL_URL_ROOT.'/viewimage.php', 'src="!~!~!~'.DOL_URL_ROOT.'/viewimage.php', $content);
+ $content = str_replace('href="document.php', 'href="!~!~!~/document.php', $content);
+ $content = str_replace('href="/document.php', 'href="!~!~!~/document.php', $content);
+ $content = str_replace('href="'.DOL_URL_ROOT.'/document.php', 'href="!~!~!~'.DOL_URL_ROOT.'/document.php', $content);
+
// Replace relative link / with dolibarr URL: ...href="/"...
- $content=preg_replace('/(href=")\/\"/', '\1'.DOL_URL_ROOT.'/public/website/index.php?website='.$website->ref.'"', $content, -1, $nbrep);
- // Replace relative link /xxx.php with dolibarr URL: ...href="....php"
- $content=preg_replace('/(href=")\/?([^:\"]*)(\.php\")/', '\1'.DOL_URL_ROOT.'/public/website/index.php?website='.$website->ref.'&pageref=\2"', $content, -1, $nbrep);
- // Replace relative link /xxx with dolibarr URL: ...href="....php"
- $content=preg_replace('/(href=")\/?([a-zA-Z0-9\-]+)(\")/', '\1'.DOL_URL_ROOT.'/public/website/index.php?website='.$website->ref.'&pageref=\2\3', $content, -1, $nbrep);
- $content=preg_replace('/(href=")\/?([a-zA-Z0-9\-]+)(\?)/', '\1'.DOL_URL_ROOT.'/public/website/index.php?website='.$website->ref.'&pageref=\2\3', $content, -1, $nbrep);
+ $content = preg_replace('/(href=")\/\"/', '\1!~!~!~'.DOL_URL_ROOT.'/public/website/index.php?website='.$website->ref.'"', $content, -1, $nbrep);
+ // Replace relative link /xxx.php#aaa or /xxx.php with dolibarr URL: ...href="....php" (we discard param ?...)
+ $content = preg_replace('/(href=")\/?([^:\"\!]*)\.php(#[^\"<>]*)?\"/', '\1!~!~!~'.DOL_URL_ROOT.'/public/website/index.php?website='.$website->ref.'&pageref=\2\3"', $content, -1, $nbrep);
+ // Replace relative link /xxx.php?a=b&c=d#aaa or /xxx.php?a=b&c=d with dolibarr URL
+ $content = preg_replace('/(href=")\/?([^:\"\!]*)\.php\?([^#\"<>]*)(#[^\"<>]*)?\"/', '\1!~!~!~'.DOL_URL_ROOT.'/public/website/index.php?website='.$website->ref.'&pageref=\2&\3\4"', $content, -1, $nbrep);
+ // Replace relative link without .php like /xxx#aaa or /xxx with dolibarr URL: ...href="....php"
+ $content = preg_replace('/(href=")\/?([a-zA-Z0-9\-_#]+)(\"|\?)/', '\1!~!~!~'.DOL_URL_ROOT.'/public/website/index.php?website='.$website->ref.'&pageref=\2\3', $content, -1, $nbrep);
// Fix relative link /document.php with correct URL after the DOL_URL_ROOT: href="/document.php?modulepart=" => href="/dolibarr/document.php?modulepart="
- $content=preg_replace('/(href=")(\/?document\.php\?[^\"]*modulepart=[^\"]*)(\")/', '\1'.DOL_URL_ROOT.'\2\3', $content, -1, $nbrep);
- $content=preg_replace('/(src=")(\/?document\.php\?[^\"]*modulepart=[^\"]*)(\")/', '\1'.DOL_URL_ROOT.'\2\3', $content, -1, $nbrep);
+ $content = preg_replace('/(href=")(\/?document\.php\?[^\"]*modulepart=[^\"]*)(\")/', '\1!~!~!~'.DOL_URL_ROOT.'\2\3', $content, -1, $nbrep);
+ $content = preg_replace('/(src=")(\/?document\.php\?[^\"]*modulepart=[^\"]*)(\")/', '\1!~!~!~'.DOL_URL_ROOT.'\2\3', $content, -1, $nbrep);
// Fix relative link /viewimage.php with correct URL after the DOL_URL_ROOT: href="/viewimage.php?modulepart=" => href="/dolibarr/viewimage.php?modulepart="
- $content=preg_replace('/(href=")(\/?viewimage\.php\?[^\"]*modulepart=[^\"]*)(\")/', '\1'.DOL_URL_ROOT.'\2\3', $content, -1, $nbrep);
- $content=preg_replace('/(src=")(\/?viewimage\.php\?[^\"]*modulepart=[^\"]*)(\")/', '\1'.DOL_URL_ROOT.'\2\3', $content, -1, $nbrep);
+ $content = preg_replace('/(href=")(\/?viewimage\.php\?[^\"]*modulepart=[^\"]*)(\")/', '\1!~!~!~'.DOL_URL_ROOT.'\2\3', $content, -1, $nbrep);
+ $content = preg_replace('/(src=")(\/?viewimage\.php\?[^\"]*modulepart=[^\"]*)(\")/', '\1!~!~!~'.DOL_URL_ROOT.'\2\3', $content, -1, $nbrep);
+ $content = preg_replace('/(url\(")(\/?viewimage\.php\?[^\"]*modulepart=[^\"]*)(\")/', '\1!~!~!~'.DOL_URL_ROOT.'\2\3', $content, -1, $nbrep);
// Fix relative link into medias with correct URL after the DOL_URL_ROOT: ../url("medias/
- $content=preg_replace('/url\((["\']?)medias\//', 'url(\1'.DOL_URL_ROOT.'/viewimage.php?modulepart=medias&file=', $content, -1, $nbrep);
- $content=preg_replace('/data-slide-bg=(["\']?)medias\//', 'data-slide-bg=\1'.DOL_URL_ROOT.'/viewimage.php?modulepart=medias&file=', $content, -1, $nbrep);
+ $content = preg_replace('/url\((["\']?)medias\//', 'url(\1!~!~!~'.DOL_URL_ROOT.'/viewimage.php?modulepart=medias&file=', $content, -1, $nbrep);
+ $content = preg_replace('/data-slide-bg=(["\']?)medias\//', 'data-slide-bg=\1!~!~!~'.DOL_URL_ROOT.'/viewimage.php?modulepart=medias&file=', $content, -1, $nbrep);
- // ]*src=")(medias\/)/', '\1'.DOL_URL_ROOT.'/viewimage.php?modulepart=medias&file=', $content, -1, $nbrep);
+ // ]*src=")\/?medias\//', '\1!~!~!~'.DOL_URL_ROOT.'/viewimage.php?modulepart=medias&file=', $content, -1, $nbrep);
// ]*src=")(?!(http|\/?viewimage|'.preg_quote(DOL_URL_ROOT, '/').'\/viewimage))/', '\1'.DOL_URL_ROOT.'/viewimage.php?modulepart=medias&file=', $content, -1, $nbrep);
+ $content = preg_replace('/( ]*src=")\/?([^:\"\!]+)\"/', '\1!~!~!~'.DOL_URL_ROOT.'/viewimage.php?modulepart=medias&file=\2"', $content, -1, $nbrep);
// ]*src=")(\/?viewimage\.php)/', '\1'.DOL_URL_ROOT.'/viewimage.php', $content, -1, $nbrep);
+ $content = preg_replace('/( ]*src=")(\/?viewimage\.php)/', '\1!~!~!~'.DOL_URL_ROOT.'/viewimage.php', $content, -1, $nbrep);
// action="newpage.php" => action="dolibarr/website/index.php?website=...&pageref=newpage
- $content = preg_replace('/(action=")\/?([^:\"]*)(\.php\")/', '\1'.DOL_URL_ROOT.'/public/website/index.php?website='.$website->ref.'&pageref=\2"', $content, -1, $nbrep);
+ $content = preg_replace('/(action=")\/?([^:\"]*)(\.php\")/', '\1!~!~!~'.DOL_URL_ROOT.'/public/website/index.php?website='.$website->ref.'&pageref=\2"', $content, -1, $nbrep);
+
+ // Fix relative URL
+ $content = str_replace('src="!~!~!~/viewimage.php', 'src="!~!~!~'.DOL_URL_ROOT.'/viewimage.php', $content);
+ $content = str_replace('href="!~!~!~/document.php', 'href="!~!~!~'.DOL_URL_ROOT.'/document.php', $content);
+ // Remove the protection tag !~!~!~
+ $content = str_replace('!~!~!~', '', $content);
}
else // REPLACEMENT OF LINKS When page called from virtual host
{
@@ -244,7 +300,12 @@ function dolWebsiteOutput($content)
$nbrep=0;
if (! $symlinktomediaexists)
{
- $content=preg_replace('/('."\n";
print ''."\n";
+ print ''."\n";
}
// Browser notifications
@@ -2337,7 +2347,7 @@ if (! function_exists("llxFooter"))
url: "https://ping.dolibarr.org/",
timeout: 500, // timeout milliseconds
cache: false,
- data: { hash_algo: "md5", hash_unique_id: "file->instance_unique_id); ?>", action: "dolibarrping", version: "", entity: entity; ?> },
+ data: { hash_algo: "md5", hash_unique_id: "file->instance_unique_id); ?>", action: "dolibarrping", version: "", entity: entity; ?> },
success: function (data, status, xhr) { // success callback function (data contains body of response)
console.log("Ping ok");
$.ajax({
diff --git a/htdocs/master.inc.php b/htdocs/master.inc.php
index 6ddb439f074..fcf68096e6b 100644
--- a/htdocs/master.inc.php
+++ b/htdocs/master.inc.php
@@ -121,6 +121,25 @@ if (! defined('NOREQUIREDB'))
if ($db->error)
{
+ // If we were into a website context
+ if (! defined('USEDOLIBARREDITOR') && ! defined('USEDOLIBARRSERVER') && ! empty($_SERVER['SCRIPT_FILENAME']) && (strpos($_SERVER['SCRIPT_FILENAME'], DOL_DATA_ROOT.'/website') === 0))
+ {
+ $sapi_type = php_sapi_name();
+ if (substr($sapi_type, 0, 3) != 'cgi') http_response_code(503); // To tel search engine this is a temporary error
+ print '';
+ if (is_object($langs))
+ {
+ $langs->setDefaultLang('auto');
+ $langs->load("website");
+ print $langs->trans("SorryWebsiteIsCurrentlyOffLine");
+ }
+ else
+ {
+ print "SorryWebsiteIsCurrentlyOffLine";
+ }
+ print '
';
+ exit;
+ }
dol_print_error($db, "host=".$conf->db->host.", port=".$conf->db->port.", user=".$conf->db->user.", databasename=".$conf->db->name.", ".$db->error);
exit;
}
@@ -245,3 +264,4 @@ $hookmanager=new HookManager($db);
if (! defined('MAIN_LABEL_MENTION_NPR') ) define('MAIN_LABEL_MENTION_NPR', 'NPR');
+//if (! defined('PCLZIP_TEMPORARY_DIR')) define('PCLZIP_TEMPORARY_DIR', $conf->user->dir_temp);
diff --git a/htdocs/product/admin/product.php b/htdocs/product/admin/product.php
index a7e45d488e1..97823bfc861 100644
--- a/htdocs/product/admin/product.php
+++ b/htdocs/product/admin/product.php
@@ -146,6 +146,7 @@ if ($action == 'other')
$value = GETPOST('activate_useProdFournDesc', 'alpha');
$res = dolibarr_set_const($db, "PRODUIT_FOURN_TEXTS", $value, 'chaine', 0, '', $conf->entity);
+
if ($value) {
$sql_test = "SELECT count(desc_fourn) as cpt FROM ".MAIN_DB_PREFIX."product_fournisseur_price WHERE 1";
$resql = $db->query($sql_test);
@@ -556,7 +557,14 @@ if (! empty($conf->fournisseur->enabled)) $rowspan++;
print '';
-print ''.$langs->trans("PricingRule").' ';
+if (empty($conf->multicompany->enabled))
+{
+ print ''.$langs->trans("PricingRule").' ';
+}
+else
+{
+ print ''.$form->textwithpicto($langs->trans("PricingRule"), $langs->trans("SamePriceAlsoForSharedCompanies"), 1).' ';
+}
print '';
$current_rule = 'PRODUCT_PRICE_UNIQ';
if (!empty($conf->global->PRODUIT_MULTIPRICES)) $current_rule='PRODUIT_MULTIPRICES';
@@ -564,10 +572,6 @@ if (!empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY)) $current_rule='PRODUI
if (!empty($conf->global->PRODUIT_CUSTOMER_PRICES)) $current_rule='PRODUIT_CUSTOMER_PRICES';
if (!empty($conf->global->PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES)) $current_rule='PRODUIT_CUSTOMER_PRICES_BY_QTY_MULTIPRICES';
print $form->selectarray("princingrule", $select_pricing_rules, $current_rule);
-if ( empty($conf->multicompany->enabled))
-{
- print $langs->trans("SamePriceAlsoForSharedCompanies");
-}
print ' ';
print ' ';
print ' ';
@@ -675,6 +679,7 @@ if (! empty($conf->fournisseur->enabled))
print ' ';
}
+
if (! empty($conf->global->PRODUCT_CANVAS_ABILITY))
{
// Add canvas feature
diff --git a/htdocs/product/card.php b/htdocs/product/card.php
index cdd94ae4a3c..5be4dc43358 100644
--- a/htdocs/product/card.php
+++ b/htdocs/product/card.php
@@ -1028,7 +1028,7 @@ else
if ($type == 1)
{
print ''.$langs->trans("Duration").' ';
- print ' ';
+ print ' ';
print $formproduct->selectMeasuringUnits("duration_unit", "time", GETPOST('duration_value', 'alpha'), 0, 1);
print ' ';
}
diff --git a/htdocs/product/class/product.class.php b/htdocs/product/class/product.class.php
index 11cbfa9ff14..8d65a88acad 100644
--- a/htdocs/product/class/product.class.php
+++ b/htdocs/product/class/product.class.php
@@ -2788,11 +2788,10 @@ class Product extends CommonObject
public function load_stats_facture($socid = 0)
{
// phpcs:enable
- global $conf;
- global $user;
+ global $db, $conf, $user;
$sql = "SELECT COUNT(DISTINCT f.fk_soc) as nb_customers, COUNT(DISTINCT f.rowid) as nb,";
- $sql.= " COUNT(fd.rowid) as nb_rows, SUM(fd.qty) as qty";
+ $sql.= " COUNT(fd.rowid) as nb_rows, SUM(".$db->ifsql('f.type != 2', 'fd.qty', 'fd.qty * -1').") as qty";
$sql.= " FROM ".MAIN_DB_PREFIX."facturedet as fd";
$sql.= ", ".MAIN_DB_PREFIX."facture as f";
$sql.= ", ".MAIN_DB_PREFIX."societe as s";
diff --git a/htdocs/product/dynamic_price/class/price_parser.class.php b/htdocs/product/dynamic_price/class/price_parser.class.php
index ef968f5e960..016917372ad 100644
--- a/htdocs/product/dynamic_price/class/price_parser.class.php
+++ b/htdocs/product/dynamic_price/class/price_parser.class.php
@@ -262,24 +262,29 @@ class PriceParser
return -1;
}
- //Get the supplier min
- $productFournisseur = new ProductFournisseur($this->db);
- $supplier_min_price = $productFournisseur->find_min_price_product_fournisseur($product->id, 0, 0);
+ //Get the supplier min price
+ $productFournisseur = new ProductFournisseur($this->db);
+ $res = $productFournisseur->find_min_price_product_fournisseur($product->id, 0, 0);
+ if ($res < 1) {
+ $this->error_parser = array(25, null);
+ return -1;
+ }
+ $supplier_min_price = $productFournisseur->fourn_unitprice;
//Accessible values by expressions
- $extra_values = array_merge($extra_values, array(
- "supplier_min_price" => $supplier_min_price,
- ));
+ $extra_values = array_merge($extra_values, array(
+ "supplier_min_price" => $supplier_min_price,
+ ));
- //Parse the expression and return the price, if not error occurred check if price is higher than min
- $result = $this->parseExpression($product, $price_expression->expression, $extra_values);
- if (empty($this->error_parser)) {
- if ($result < $product->price_min) {
- $result = $product->price_min;
- }
- }
- return $result;
- }
+ //Parse the expression and return the price, if not error occurred check if price is higher than min
+ $result = $this->parseExpression($product, $price_expression->expression, $extra_values);
+ if (empty($this->error_parser)) {
+ if ($result < $product->price_min) {
+ $result = $product->price_min;
+ }
+ }
+ return $result;
+ }
/**
* Calculates supplier product price based on product supplier price and associated expression
diff --git a/htdocs/product/list.php b/htdocs/product/list.php
index 1a9b5fa1f6f..e7e8f1e4999 100644
--- a/htdocs/product/list.php
+++ b/htdocs/product/list.php
@@ -136,6 +136,7 @@ $fieldstosearchall = array(
'p.label'=>"ProductLabel",
'p.description'=>"Description",
"p.note"=>"Note",
+
);
// multilang
if (! empty($conf->global->MAIN_MULTILANGS))
@@ -148,6 +149,8 @@ if (! empty($conf->barcode->enabled)) {
$fieldstosearchall['p.barcode']='Gencod';
$fieldstosearchall['pfp.barcode']='GencodBuyPrice';
}
+// Personalized search criterias. Example: $conf->global->PRODUCT_QUICKSEARCH_ON_FIELDS = 'p.ref=ProductRef;p.label=ProductLabel'
+if (! empty($conf->global->PRODUCT_QUICKSEARCH_ON_FIELDS)) $fieldstosearchall=dolExplodeIntoArray($conf->global->PRODUCT_QUICKSEARCH_ON_FIELDS);
if (empty($conf->global->PRODUIT_MULTIPRICES))
{
@@ -455,6 +458,10 @@ if ($resql)
if($type == Product::TYPE_SERVICE) $rightskey='service';
if($user->rights->{$rightskey}->creer)
{
+ if ($type === "") {
+ $newcardbutton.= dolGetButtonTitle($langs->trans('NewProduct'), '', 'fa fa-plus-circle', DOL_URL_ROOT.'/product/card.php?action=create&type=0');
+ $type = Product::TYPE_SERVICE;
+ }
$label='NewProduct';
if($type == Product::TYPE_SERVICE) $label='NewService';
$newcardbutton.= dolGetButtonTitle($langs->trans($label), '', 'fa fa-plus-circle', DOL_URL_ROOT.'/product/card.php?action=create&type='.$type);
diff --git a/htdocs/product/stats/facture.php b/htdocs/product/stats/facture.php
index 00f9c84338e..ed24647ad67 100644
--- a/htdocs/product/stats/facture.php
+++ b/htdocs/product/stats/facture.php
@@ -236,7 +236,8 @@ if ($id > 0 || ! empty($ref))
$objp = $db->fetch_object($result);
if ($objp->type == Facture::TYPE_CREDIT_NOTE) $objp->qty=-($objp->qty);
- $total_ht+=$objp->total_ht;
+
+ $total_ht+=$objp->total_ht;
$total_qty+=$objp->qty;
$invoicestatic->id=$objp->facid;
diff --git a/htdocs/product/stock/card.php b/htdocs/product/stock/card.php
index 89d7258e3cc..958f03dff2f 100644
--- a/htdocs/product/stock/card.php
+++ b/htdocs/product/stock/card.php
@@ -56,6 +56,9 @@ $backtopage=GETPOST('backtopage', 'alpha');
//$result=restrictedArea($user,'stock', $id, 'entrepot&stock');
$result=restrictedArea($user, 'stock');
+// Initialize technical object to manage hooks of page. Note that conf->hooks_modules contains array of hook context
+$hookmanager->initHooks(array('warehousecard', 'globalcard'));
+
$object = new Entrepot($db);
$extrafields = new ExtraFields($db);
@@ -73,8 +76,6 @@ if ($id > 0 || ! empty($ref)) {
}
}
-// Initialize technical object to manage hooks of page. Note that conf->hooks_modules contains array of hook context
-$hookmanager->initHooks(array('warehousecard','globalcard'));
/*
* Actions
diff --git a/htdocs/product/stock/class/mouvementstock.class.php b/htdocs/product/stock/class/mouvementstock.class.php
index c6db985f515..1ddf58cde2b 100644
--- a/htdocs/product/stock/class/mouvementstock.class.php
+++ b/htdocs/product/stock/class/mouvementstock.class.php
@@ -344,17 +344,31 @@ class MouvementStock extends CommonObject
if ($movestock && $entrepot_id > 0) // Change stock for current product, change for subproduct is done after
{
+ $fk_project = 0;
if(!empty($this->origin)) { // This is set by caller for tracking reason
$origintype = $this->origin->element;
$fk_origin = $this->origin->id;
+ if($origintype == 'project') $fk_project = $fk_origin;
+ else
+ {
+ $res = $this->origin->fetch($fk_origin);
+ if ($res > 0)
+ {
+ if (!empty($this->origin->fk_project))
+ {
+ $fk_project = $this->origin->fk_project;
+ }
+ }
+ }
} else {
$origintype = '';
$fk_origin = 0;
+ $fk_project = 0;
}
$sql = "INSERT INTO ".MAIN_DB_PREFIX."stock_mouvement(";
$sql.= " datem, fk_product, batch, eatby, sellby,";
- $sql.= " fk_entrepot, value, type_mouvement, fk_user_author, label, inventorycode, price, fk_origin, origintype";
+ $sql.= " fk_entrepot, value, type_mouvement, fk_user_author, label, inventorycode, price, fk_origin, origintype, fk_projet";
$sql.= ")";
$sql.= " VALUES ('".$this->db->idate($now)."', ".$this->product_id.", ";
$sql.= " ".($batch?"'".$batch."'":"null").", ";
@@ -366,7 +380,8 @@ class MouvementStock extends CommonObject
$sql.= " ".($inventorycode?"'".$this->db->escape($inventorycode)."'":"null").",";
$sql.= " '".price2num($price)."',";
$sql.= " '".$fk_origin."',";
- $sql.= " '".$origintype."'";
+ $sql.= " '".$origintype."',";
+ $sql.= " ". $fk_project;
$sql.= ")";
dol_syslog(get_class($this)."::_create insert record into stock_mouvement", LOG_DEBUG);
@@ -569,7 +584,8 @@ class MouvementStock extends CommonObject
$sql .= " t.inventorycode,";
$sql .= " t.batch,";
$sql .= " t.eatby,";
- $sql .= " t.sellby";
+ $sql .= " t.sellby,";
+ $sql .= " t.fk_projet";
$sql .= ' FROM ' . MAIN_DB_PREFIX . $this->table_element . ' as t';
$sql.= ' WHERE 1 = 1';
//if (null !== $ref) {
diff --git a/htdocs/product/stock/movement_list.php b/htdocs/product/stock/movement_list.php
index efcbc534c08..f62b732cf7e 100644
--- a/htdocs/product/stock/movement_list.php
+++ b/htdocs/product/stock/movement_list.php
@@ -112,6 +112,7 @@ $arrayfields=array(
'origin'=>array('label'=>$langs->trans("Origin"), 'checked'=>1),
'm.value'=>array('label'=>$langs->trans("Qty"), 'checked'=>1),
'm.price'=>array('label'=>$langs->trans("UnitPurchaseValue"), 'checked'=>0),
+ 'm.fk_projet'=>array('label'=>$langs->trans('Project'), 'checked'=>0)
//'m.datec'=>array('label'=>$langs->trans("DateCreation"), 'checked'=>0, 'position'=>500),
//'m.tms'=>array('label'=>$langs->trans("DateModificationShort"), 'checked'=>0, 'position'=>500)
);
@@ -423,6 +424,7 @@ $sql.= " e.ref as stock, e.rowid as entrepot_id, e.lieu,";
$sql.= " m.rowid as mid, m.value as qty, m.datem, m.fk_user_author, m.label, m.inventorycode, m.fk_origin, m.origintype,";
$sql.= " m.batch, m.price,";
$sql.= " m.type_mouvement,";
+$sql.= " m.fk_projet,";
$sql.= " pl.rowid as lotid, pl.eatby, pl.sellby,";
$sql.= " u.login, u.photo, u.lastname, u.firstname";
// Add fields from extrafields
@@ -854,7 +856,14 @@ if ($resql)
if (! empty($arrayfields['m.price']['checked']))
{
// Price
- print '';
+ print ' ';
+ print ' ';
+ print ' ';
+ }
+ if (! empty($arrayfields['m.fk_projet']['checked']))
+ {
+ // fk_projet
+ print '';
print ' ';
print ' ';
}
@@ -933,6 +942,9 @@ if ($resql)
if (! empty($arrayfields['m.price']['checked'])) {
print_liste_field_titre($arrayfields['m.price']['label'], $_SERVER["PHP_SELF"], "m.price", "", $param, '', $sortfield, $sortorder, 'right ');
}
+ if (! empty($arrayfields['m.fk_projet']['checked'])) {
+ print_liste_field_titre($arrayfields['m.fk_projet']['label'], $_SERVER["PHP_SELF"], "m.fk_projet", "", $param, 'align="right"', $sortfield, $sortorder);
+ }
// Extra fields
include DOL_DOCUMENT_ROOT.'/core/tpl/extrafields_list_search_title.tpl.php';
@@ -1100,6 +1112,13 @@ if ($resql)
if ($objp->price != 0) print price($objp->price);
print '';
}
+ if (! empty($arrayfields['m.fk_projet']['checked']))
+ {
+ // fk_projet
+ print '';
+ if ($objp->fk_projet != 0) print $movement->get_origin($objp->fk_projet, 'project');
+ print ' ';
+ }
// Action column
print '';
if ($massactionbutton || $massaction) // If we are in select mode (massactionbutton defined) or if we have already selected and sent an action ($massaction) defined
diff --git a/htdocs/societe/class/societe.class.php b/htdocs/societe/class/societe.class.php
index 6d936e5ce65..01770e1bee1 100644
--- a/htdocs/societe/class/societe.class.php
+++ b/htdocs/societe/class/societe.class.php
@@ -2167,8 +2167,8 @@ class Societe extends CommonObject
{
$label.= '' . $langs->trans('Name') . ': '. $this->name;
if (! empty($this->name_alias)) $label.=' ('.$this->name_alias.')';
- $label.= '' . $langs->trans('Email') . ': '. $this->email;
}
+ $label.= '' . $langs->trans('Email') . ': '. $this->email;
if (! empty($this->country_code))
$label.= '' . $langs->trans('Country') . ': '. $this->country_code;
if (! empty($this->tva_intra) || (! empty($conf->global->SOCIETE_SHOW_FIELD_IN_TOOLTIP) && strpos($conf->global->SOCIETE_SHOW_FIELD_IN_TOOLTIP, 'vatnumber') !== false))
diff --git a/htdocs/societe/list.php b/htdocs/societe/list.php
index 2ee5f5bedf5..2ab86d454f0 100644
--- a/htdocs/societe/list.php
+++ b/htdocs/societe/list.php
@@ -596,6 +596,7 @@ print ' ';
print ' ';
print ' ';
+print ' ';
print_barre_liste($title, $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, $massactionbutton, $num, $nbtotalofrecords, 'title_companies', 0, $newcardbutton, '', $limit);
diff --git a/htdocs/societe/paymentmodes.php b/htdocs/societe/paymentmodes.php
index 4f525330e79..8e1ad71f5b0 100644
--- a/htdocs/societe/paymentmodes.php
+++ b/htdocs/societe/paymentmodes.php
@@ -788,10 +788,8 @@ if ($socid && $action != 'edit' && $action != 'create' && $action != 'editcard'
$permissiontowrite = $user->rights->societe->creer;
// Stripe customer key 'cu_....' stored into llx_societe_account
print ' ';
- //print $langs->trans('StripeCustomerId');
print $form->editfieldkey("StripeCustomerId", 'key_account', $stripecu, $object, $permissiontowrite, 'string', '', 0, 2, 'socid');
print ' ';
- //print $stripecu;
print $form->editfieldval("StripeCustomerId", 'key_account', $stripecu, $object, $permissiontowrite, 'string', '', null, null, '', 2, '', 'socid');
if (! empty($conf->stripe->enabled) && $stripecu && $action != 'editkey_account')
{
@@ -817,6 +815,60 @@ if ($socid && $action != 'edit' && $action != 'create' && $action != 'editcard'
print ' ';
}
}
+
+ if ($object->fournisseur)
+ {
+ print '';
+ print $langs->trans('SupplierCode').' ';
+ print $object->code_fournisseur;
+ if ($object->check_codefournisseur() <> 0) print ' ('.$langs->trans("WrongSupplierCode").') ';
+ print ' ';
+ $sql = "SELECT count(*) as nb from ".MAIN_DB_PREFIX."facture where fk_soc = ".$socid;
+ $resql=$db->query($sql);
+ if (!$resql) dol_print_error($db);
+ $obj = $db->fetch_object($resql);
+ $nbFactsClient = $obj->nb;
+ $thirdTypeArray['customer']=$langs->trans("customer");
+ if ($conf->propal->enabled && $user->rights->propal->lire) $elementTypeArray['propal']=$langs->transnoentitiesnoconv('Proposals');
+ if ($conf->commande->enabled && $user->rights->commande->lire) $elementTypeArray['order']=$langs->transnoentitiesnoconv('Orders');
+ if ($conf->facture->enabled && $user->rights->facture->lire) $elementTypeArray['invoice']=$langs->transnoentitiesnoconv('Invoices');
+ if ($conf->contrat->enabled && $user->rights->contrat->lire) $elementTypeArray['contract']=$langs->transnoentitiesnoconv('Contracts');
+ }
+
+ if (! empty($conf->stripe->enabled) && ! empty($conf->stripeconnect->enabled) && $conf->global->MAIN_FEATURES_LEVEL >= 2)
+ {
+ $permissiontowrite = $user->rights->societe->creer;
+ $stripesupplieracc = $stripe->getStripeAccount($service, $object->id); // Get Stripe OAuth connect account (no network access here)
+
+ // Stripe customer key 'cu_....' stored into llx_societe_account
+ print '';
+ print $form->editfieldkey("StripeConnectAccount", 'key_account_supplier', $stripesupplieracc, $object, $permissiontowrite, 'string', '', 0, 2, 'socid');
+ print ' ';
+ print $form->editfieldval("StripeConnectAccount", 'key_account_supplier', $stripesupplieracc, $object, $permissiontowrite, 'string', '', null, null, '', 2, '', 'socid');
+ if (! empty($conf->stripe->enabled) && $stripesupplieracc && $action != 'editkey_account')
+ {
+ $connect='';
+
+ $url='https://dashboard.stripe.com/test/connect/accounts/'.$stripesupplieracc;
+ if ($servicestatus)
+ {
+ $url='https://dashboard.stripe.com/connect/accounts/'.$stripesupplieracc;
+ }
+ print ' '.img_picto($langs->trans('ShowInStripe'), 'object_globe').' ';
+ }
+ print ' ';
+ if (empty($stripesupplieracc))
+ {
+ print '';
+ }
+ print ' ';
+ }
print '';
print ' ';
@@ -826,7 +878,7 @@ if ($socid && $action != 'edit' && $action != 'create' && $action != 'editcard'
print '